Browse Source

Merge remote-tracking branch 'origin/develop' into webv2

pull/2032/head
Gabe Kangas 4 years ago
parent
commit
506d1fa4cf
No known key found for this signature in database
GPG Key ID: 9A56337728BC81EA
  1. 40
      .github/workflows/docker-nightly-earthly.yaml
  2. 26
      .github/workflows/docker-nightly.yaml
  3. 4
      activitypub/resolvers/resolve.go
  4. 14
      build/release/docker-nightly-earthly.sh
  5. 21
      build/release/docker-nightly.sh
  6. 6
      openapi.yaml
  7. 4
      router/router.go
  8. 1
      static/web/_next/static/chunks/1758-3a8e1364ffda64ee.js
  9. 1
      static/web/_next/static/chunks/6132-0f911799dd6dd847.js
  10. 1
      static/web/_next/static/chunks/pages/_app-e57b2a440c783b12.js
  11. 156
      test/automated/api/chatmoderation.test.js
  12. 1088
      test/automated/api/package-lock.json
  13. 1151
      test/automated/hls/package-lock.json
  14. 6
      test/load/package-lock.json
  15. 2
      web/utils/constants.js
  16. 229
      webroot/js/components/federation/followers.js

40
.github/workflows/docker-nightly-earthly.yaml

@ -1,40 +0,0 @@ @@ -1,40 +0,0 @@
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
# for details.
name: Build nightly docker
on:
workflow_dispatch:
schedule:
- cron: '0 2 * * *'
jobs:
Docker:
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
with:
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
- name: Earthly version
run: earthly --version
- name: Log into GitHub Container Registry
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
if: env.GH_CR_PAT != null
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt:latest
platforms: all
- uses: actions/checkout@v3
- name: Checkout and build
if: env.GH_CR_PAT != null
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: cd build/release && ./docker-nightly-earthly.sh

26
.github/workflows/docker-nightly.yaml

@ -1,24 +1,40 @@ @@ -1,24 +1,40 @@
# See https://docs.earthly.dev/ci-integration/vendor-specific-guides/gh-actions-integration
# for details.
name: Build nightly docker
on:
workflow_dispatch:
schedule:
- cron: "0 2 * * *"
- cron: '0 2 * * *'
jobs:
Docker:
runs-on: ubuntu-latest
steps:
- uses: earthly/actions-setup@v1
with:
version: 'latest' # or pin to an specific version, e.g. "v0.6.10"
- name: Earthly version
run: earthly --version
- name: Log into GitHub Container Registry
env:
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: echo "${{ secrets.GH_CR_PAT }}" | docker login https://ghcr.io -u ${{ github.actor }} --password-stdin
if: env.GH_CR_PAT != null
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt:latest
platforms: all
- uses: actions/checkout@v3
- name: Setup and run
env:
- name: Checkout and build
if: env.GH_CR_PAT != null
env:
GH_CR_PAT: ${{ secrets.GH_CR_PAT }}
run: cd build/release && ./docker-nightly.sh
if: env.GH_CR_PAT != null

4
activitypub/resolvers/resolve.go

@ -178,11 +178,11 @@ func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1Publi @@ -178,11 +178,11 @@ func GetResolvedPublicKeyFromIRI(publicKeyIRI string) (vocab.W3IDSecurityV1Publi
}
if err != nil {
err = errors.Wrap(err, "error resolving publickey from iri")
err = errors.Wrap(err, "error resolving publickey from iri, actor may not be valid: "+publicKeyIRI)
}
if !resolved {
err = errors.New("error resolving publickey from iri")
err = errors.New("error resolving publickey from iri, actor may not be valid: " + publicKeyIRI)
}
return pubkey, err

14
build/release/docker-nightly-earthly.sh

@ -1,14 +0,0 @@ @@ -1,14 +0,0 @@
#!/bin/sh
# Docker build
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
DOCKER_IMAGE="owncast-earthly"
DATE=$(date +"%Y%m%d")
VERSION="${DATE}-nightly"
echo "Building Docker image ${DOCKER_IMAGE}..."
# Change to the root directory of the repository
cd $(git rev-parse --show-toplevel)
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"

21
build/release/docker-nightly.sh

@ -1,29 +1,14 @@ @@ -1,29 +1,14 @@
#!/bin/sh
# Docker build
# Must authenticate first: https://docs.github.com/en/packages/using-github-packages-with-your-projects-ecosystem/configuring-docker-for-use-with-github-packages#authenticating-to-github-packages
DOCKER_IMAGE="owncast"
DATE=$(date +"%Y%m%d")
VERSION="${DATE}-nightly"
GIT_COMMIT=$(git rev-list -1 HEAD)
# Create production build of Tailwind CSS
pushd ../../build/javascript >> /dev/null
# Install the tailwind & postcss CLIs
npm install --quiet --no-progress
# Run the tailwind CLI and pipe it to postcss for minification.
# Save it to a temp directory that we will reference below.
NODE_ENV="production" ./node_modules/.bin/tailwind build | ./node_modules/.bin/postcss > "../../webroot/js/web_modules/tailwindcss/dist/tailwind.min.css"
popd
echo "Building Docker image ${DOCKER_IMAGE}..."
# Change to the root directory of the repository
cd $(git rev-parse --show-toplevel)
# Docker build
docker build --build-arg NAME=docker --build-arg VERSION=${VERSION} --build-arg GIT_COMMIT=$GIT_COMMIT -t ghcr.io/owncast/${DOCKER_IMAGE}:nightly .
# Dockerhub
# You must be authenticated via `docker login` with your Dockerhub credentials first.
# docker push gabekangas/owncast:nightly
docker push ghcr.io/owncast/${DOCKER_IMAGE}:nightly
earthly --ci --push +docker-all --image="ghcr.io/owncast/${DOCKER_IMAGE}" --tag=nightly --version="${VERSION}"

6
openapi.yaml

@ -686,7 +686,7 @@ paths: @@ -686,7 +686,7 @@ paths:
type: string
example: https://fediverse.biz/authorize_interaction?uri=https://my.owncast.server/federation/user/streamer
/api/chat/updatemessagevisibility:
/api/chat/messagevisibility:
post:
summary: Update the visibility of chat messages.
description: Pass an array of IDs you want to change the chat visibility of.
@ -951,7 +951,7 @@ paths: @@ -951,7 +951,7 @@ paths:
type: string
format: date-time
/api/admin/chat/updatemessagevisibility:
/api/admin/chat/messagevisibility:
post:
summary: Update the visibility of chat messages.
description: Pass an array of IDs you want to change the chat visibility of.
@ -1984,7 +1984,7 @@ paths: @@ -1984,7 +1984,7 @@ paths:
type: string
format: date-time
/api/integrations/chat/updatemessagevisibility:
/api/integrations/chat/messagevisibility:
post:
summary: Update the visibility of chat messages.
description: Pass an array of IDs you want to change the chat visibility of.

4
router/router.go

@ -131,7 +131,7 @@ func Start() error { @@ -131,7 +131,7 @@ func Start() error {
http.HandleFunc("/api/admin/chat/messages", middleware.RequireAdminAuth(admin.GetChatMessages))
// Update chat message visibility
http.HandleFunc("/api/admin/chat/updatemessagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility))
http.HandleFunc("/api/admin/chat/messagevisibility", middleware.RequireAdminAuth(admin.UpdateMessageVisibility))
// Enable/disable a user
http.HandleFunc("/api/admin/chat/users/setenabled", middleware.RequireAdminAuth(admin.UpdateUserEnabled))
@ -318,7 +318,7 @@ func Start() error { @@ -318,7 +318,7 @@ func Start() error {
// Inline chat moderation actions
// Update chat message visibility
http.HandleFunc("/api/chat/updatemessagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility))
http.HandleFunc("/api/chat/messagevisibility", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateMessageVisibility))
// Enable/disable a user
http.HandleFunc("/api/chat/users/setenabled", middleware.RequireUserModerationScopeAccesstoken(admin.UpdateUserEnabled))

1
static/web/_next/static/chunks/1758-3a8e1364ffda64ee.js

File diff suppressed because one or more lines are too long

1
static/web/_next/static/chunks/6132-0f911799dd6dd847.js

File diff suppressed because one or more lines are too long

1
static/web/_next/static/chunks/pages/_app-e57b2a440c783b12.js

File diff suppressed because one or more lines are too long

156
test/automated/api/chatmoderation.test.js

@ -7,112 +7,112 @@ const registerChat = require('./lib/chat').registerChat; @@ -7,112 +7,112 @@ const registerChat = require('./lib/chat').registerChat;
const sendChatMessage = require('./lib/chat').sendChatMessage;
const testVisibilityMessage = {
body: 'message ' + Math.floor(Math.random() * 100),
type: 'CHAT',
body: 'message ' + Math.floor(Math.random() * 100),
type: 'CHAT',
};
var messageId;
const establishedUserFailedChatMessage = {
body: 'this message should fail to send ' + Math.floor(Math.random() * 100),
type: 'CHAT',
body: 'this message should fail to send ' + Math.floor(Math.random() * 100),
type: 'CHAT',
};
test('can send a chat message', async (done) => {
const registration = await registerChat();
const accessToken = registration.accessToken;
const registration = await registerChat();
const accessToken = registration.accessToken;
sendChatMessage(testVisibilityMessage, accessToken, done);
sendChatMessage(testVisibilityMessage, accessToken, done);
});
test('verify we can make API call to mark message as hidden', async (done) => {
const registration = await registerChat();
const accessToken = registration.accessToken;
const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`,
{
origin: 'http://localhost:8080',
}
);
// Verify the visibility change comes through the websocket
ws.on('message', async function incoming(message) {
const messages = message.split('\n');
messages.forEach(async function (message) {
const event = JSON.parse(message);
if (event.type === 'VISIBILITY-UPDATE') {
ws.close();
done();
}
});
});
const res = await request
.get('/api/admin/chat/messages')
.auth('admin', 'abc123')
.expect(200);
const message = res.body[0];
messageId = message.id;
await request
.post('/api/admin/chat/updatemessagevisibility')
.auth('admin', 'abc123')
.send({ idArray: [messageId], visible: false })
.expect(200);
const registration = await registerChat();
const accessToken = registration.accessToken;
const ws = new WebSocket(
`ws://localhost:8080/ws?accessToken=${accessToken}`,
{
origin: 'http://localhost:8080',
}
);
// Verify the visibility change comes through the websocket
ws.on('message', async function incoming(message) {
const messages = message.split('\n');
messages.forEach(async function (message) {
const event = JSON.parse(message);
if (event.type === 'VISIBILITY-UPDATE') {
ws.close();
done();
}
});
});
const res = await request
.get('/api/admin/chat/messages')
.auth('admin', 'abc123')
.expect(200);
const message = res.body[0];
messageId = message.id;
await request
.post('/api/admin/chat/messagevisibility')
.auth('admin', 'abc123')
.send({ idArray: [messageId], visible: false })
.expect(200);
});
test('verify message has become hidden', async (done) => {
await new Promise((r) => setTimeout(r, 2000));
const res = await request
.get('/api/admin/chat/messages')
.expect(200)
.auth('admin', 'abc123');
const message = res.body.filter((obj) => {
return obj.id === messageId;
});
expect(message.length).toBe(1);
// expect(message[0].hiddenAt).toBeTruthy();
done();
await new Promise((r) => setTimeout(r, 2000));
const res = await request
.get('/api/admin/chat/messages')
.expect(200)
.auth('admin', 'abc123');
const message = res.body.filter((obj) => {
return obj.id === messageId;
});
expect(message.length).toBe(1);
// expect(message[0].hiddenAt).toBeTruthy();
done();
});
test('can enable established chat user mode', async (done) => {
await request
.post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123')
.send({ value: true })
.expect(200);
done();
await request
.post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123')
.send({ value: true })
.expect(200);
done();
});
test('can send a message after established user mode is enabled', async (done) => {
const registration = await registerChat();
const accessToken = registration.accessToken;
const registration = await registerChat();
const accessToken = registration.accessToken;
sendChatMessage(establishedUserFailedChatMessage, accessToken, done);
sendChatMessage(establishedUserFailedChatMessage, accessToken, done);
});
test('verify rejected message is not in the chat feed', async (done) => {
const res = await request
.get('/api/admin/chat/messages')
.expect(200)
.auth('admin', 'abc123');
const res = await request
.get('/api/admin/chat/messages')
.expect(200)
.auth('admin', 'abc123');
const message = res.body.filter((obj) => {
return obj.body === establishedUserFailedChatMessage.body;
});
const message = res.body.filter((obj) => {
return obj.body === establishedUserFailedChatMessage.body;
});
expect(message.length).toBe(0);
done();
expect(message.length).toBe(0);
done();
});
test('can disable established chat user mode', async (done) => {
await request
.post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123')
.send({ value: false })
.expect(200);
done();
await request
.post('/api/admin/config/chat/establishedusermode')
.auth('admin', 'abc123')
.send({ value: false })
.expect(200);
done();
});

1088
test/automated/api/package-lock.json generated

File diff suppressed because it is too large Load Diff

1151
test/automated/hls/package-lock.json generated

File diff suppressed because it is too large Load Diff

6
test/load/package-lock.json generated

@ -1188,9 +1188,9 @@ @@ -1188,9 +1188,9 @@
"integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="
},
"moment": {
"version": "2.29.2",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz",
"integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg=="
"version": "2.29.4",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz",
"integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w=="
},
"ms": {
"version": "2.1.2",

2
web/utils/constants.js

@ -7,7 +7,7 @@ export const URL_CONFIG = `/api/config`; @@ -7,7 +7,7 @@ export const URL_CONFIG = `/api/config`;
export const URL_VIEWER_PING = `/api/ping`;
// inline moderation actions
export const URL_HIDE_MESSAGE = `/api/chat/updatemessagevisibility`;
export const URL_HIDE_MESSAGE = `/api/chat/messagevisibility`;
export const URL_BAN_USER = `/api/chat/users/setenabled`;
// TODO: This directory is customizable in the config. So we should expose this via the config API.

229
webroot/js/components/federation/followers.js

@ -3,132 +3,133 @@ import htm from '/js/web_modules/htm.js'; @@ -3,132 +3,133 @@ import htm from '/js/web_modules/htm.js';
import { URL_FOLLOWERS } from '/js/utils/constants.js';
const html = htm.bind(h);
export default class FollowerList extends Component {
constructor(props) {
super(props);
constructor(props) {
super(props);
this.state = {
followers: [],
followersPage: 0,
currentPage: 0,
total: 0,
};
}
this.state = {
followers: [],
followersPage: 0,
currentPage: 0,
total: 0,
};
}
componentDidMount() {
try {
this.getFollowers();
} catch (e) {
console.error('followers error: ', e);
}
}
componentDidMount() {
try {
this.getFollowers();
} catch (e) {
console.error('followers error: ', e);
}
}
async getFollowers() {
const { currentPage } = this.state;
const limit = 24;
const offset = currentPage * limit;
const u = `${URL_FOLLOWERS}?offset=${offset}&limit=${limit}`;
const response = await fetch(u);
const followers = await response.json();
async getFollowers(requestedPage) {
const limit = 24;
const offset = requestedPage * limit;
const u = `${URL_FOLLOWERS}?offset=${offset || 0}&limit=${limit}`;
const response = await fetch(u);
const followers = await response.json();
const pages = Math.ceil(followers.total / limit);
this.setState({
followers: followers.results,
total: response.total,
});
}
this.setState({
followers: followers.results,
total: followers.total,
pages: pages,
});
}
changeFollowersPage(page) {
this.setState({ currentPage: page });
this.getFollowers();
}
changeFollowersPage(requestedPage) {
this.setState({ currentPage: requestedPage });
this.getFollowers(requestedPage);
}
render() {
const { followers, total, currentPage } = this.state;
if (!followers) {
return null;
}
render() {
const { followers, total, pages, currentPage } = this.state;
if (!followers) {
return null;
}
const noFollowersInfo = html`<div class="col-span-4">
<p class="mb-5 text-2xl">Be the first to follow this live stream.</p>
<p class="text-md">
By following this stream you'll get updates when it goes live, receive
posts from the streamer, and be featured here as a follower.
</p>
<p class="text-md mt-5">
Learn more about ${' '}
<a class="underline" href="https://en.wikipedia.org/wiki/Fediverse"
>The Fediverse</a
>, where you can follow this server as well as so much more.
</p>
</div>`;
const noFollowersInfo = html`<div class="col-span-4">
<p class="mb-5 text-2xl">Be the first to follow this live stream.</p>
<p class="text-md">
By following this stream you'll get updates when it goes live, receive
posts from the streamer, and be featured here as a follower.
</p>
<p class="text-md mt-5">
Learn more about ${' '}
<a class="underline" href="https://en.wikipedia.org/wiki/Fediverse"
>The Fediverse</a
>, where you can follow this server as well as so much more.
</p>
</div>`;
const paginationControls =
total > 1 &&
Array(total)
.fill()
.map((x, n) => {
const activePageClass =
n === currentPage &&
'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white';
return html` <li class="page-item active w-10">
<a
class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}"
onClick=${() => this.changeFollowersPage(n)}
>
${n + 1}
</a>
</li>`;
});
const paginationControls =
pages > 1 &&
Array(pages)
.fill()
.map((x, n) => {
const activePageClass =
n === currentPage &&
'bg-indigo-600 rounded-full shadow-md focus:shadow-md text-white';
return html` <li class="page-item active w-10">
<a
class="page-link relative block cursor-pointer hover:no-underline py-1.5 px-3 border-0 rounded-full hover:text-gray-800 hover:bg-gray-200 outline-none transition-all duration-300 ${activePageClass}"
onClick=${() => this.changeFollowersPage(n)}
>
${n + 1}
</a>
</li>`;
});
return html`
<div>
<div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
${followers.length === 0 && noFollowersInfo}
${followers.map((follower) => {
return html` <${SingleFollower} user=${follower} /> `;
})}
</div>
<div class="flex">
<nav aria-label="Tab pages">
<ul class="flex list-style-none flex-wrap">
${paginationControls}
</ul>
</nav>
</div>
</div>
`;
}
return html`
<div>
<div
class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
>
${followers.length === 0 && noFollowersInfo}
${followers.map((follower) => {
return html` <${SingleFollower} user=${follower} /> `;
})}
</div>
<div class="flex">
<nav aria-label="Tab pages">
<ul class="flex list-style-none flex-wrap">
${paginationControls}
</ul>
</nav>
</div>
</div>
`;
}
}
function SingleFollower(props) {
const { user } = props;
const { name, username, link, image } = user;
const { user } = props;
const { name, username, link, image } = user;
var displayName = name;
var displayUsername = username;
var displayName = name;
var displayUsername = username;
if (!displayName) {
displayName = displayUsername.split('@', 1)[0];
}
return html`
<a
href=${link}
class="following-list-follower block bg-white flex p-2 rounded-xl shadow border hover:no-underline mb-3 mr-3"
target="_blank"
>
<img
src="${image || '/img/logo.svg'}"
class="w-16 h-16 rounded-full"
onError=${({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = '/img/logo.svg';
}}
/>
<div class="p-3 truncate flex-grow">
<p class="font-semibold text-gray-700 truncate">${displayName}</p>
<p class="text-sm text-gray-500 truncate">${displayUsername}</p>
</div>
</a>
`;
if (!displayName) {
displayName = displayUsername.split('@', 1)[0];
}
return html`
<a
href=${link}
class="following-list-follower block bg-white flex p-2 rounded-xl shadow border hover:no-underline mb-3 mr-3"
target="_blank"
>
<img
src="${image || '/img/logo.svg'}"
class="w-16 h-16 rounded-full"
onError=${({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = '/img/logo.svg';
}}
/>
<div class="p-3 truncate flex-grow">
<p class="font-semibold text-gray-700 truncate">${displayName}</p>
<p class="text-sm text-gray-500 truncate">${displayUsername}</p>
</div>
</a>
`;
}

Loading…
Cancel
Save