Commit 8db17e4b authored by Eliot Berriot's avatar Eliot Berriot
Browse files

Merge branch 'peertube' into 'master'

Added support for PeerTube

See merge request !1
parents 422eaa7d d2bbf46f
Pipeline #4254 passed with stages
in 1 minute and 22 seconds
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg height="682.68799" viewBox="2799 -911 512 682.688" width="512" xmlns="http://www.w3.org/2000/svg"><g stroke-width="32"><path d="m2799-911v341.344l256-170.656" fill="#211f20"/><path d="m2799-569.656v341.344l256-170.656" fill="#737373"/><path d="m3055-740.344v341.344l256-170.656" fill="#f1680d"/></g></svg>
\ No newline at end of file
<template>
<form @submit.prevent="submit">
<div class="row">
<div class="input-field col s12 l4">
<input v-model="domain" id="domain" placeholder="peertube.domain" type="text" class="validate">
<label class="active" for="domain">PeerTube domain</label>
</div>
<div class="input-field col s12 l4">
<input v-model="username" id="username" type="text" class="validate">
<label for="username">Username</label>
</div>
<div class="input-field col s12 l4">
<input v-model="password" id="password" type="password" class="validate">
<label for="password">Password</label>
</div>
</div>
<p>You're password is only used to obtain the first authentication token and isn't stored.</p>
<button type="submit" class="waves-effect waves-light btn">Authenticate</button>
</form>
</template>
<script>
import axios from 'axios'
import {createApp, getAxios, connect} from '@/sources/peertube'
import {getBaseUrl} from '@/utils'
export default {
props: ["source"],
data () {
return {
domain: '',
username: '',
password: '',
}
},
mounted () {
M.updateTextFields();
},
methods: {
async submit () {
let instanceUrl = `https://${this.domain}/`
// create an application
let ax = axios.create({
baseURL: instanceUrl,
timeout: 3000,
})
let response = await createApp(ax)
const appData = response.data
this.$store.commit(
'setRecursiveState',
{key: "sources.peertube.appCredentials", suffix: this.domain, value: appData}
)
await connect({
domain: this.domain,
router: this.$router,
store: this.$store,
username: this.username,
password: this.password
})
}
}
}
</script>
......@@ -62,7 +62,6 @@
<a v-if="retributeProfiles[suggestion.fullId] === undefined" @click="lookup(suggestion.fullId)" class="secondary-content"><i class="material-icons">search</i></a>
<div v-else-if="retributeProfiles[suggestion.fullId]">
<h6>Donation platforms</h6>
<!-- {{ retributeProfiles[suggestion.fullId] }} -->
<template v-for="mean in retributeProfiles[suggestion.fullId].means">
<a
:href="mean.url"
......@@ -95,6 +94,7 @@ import config from '@/config'
import orderBy from 'lodash/orderBy'
import defaults from 'lodash/defaults'
import pull from 'lodash/pull'
import merge from 'lodash/merge'
import chunk from 'lodash/chunk'
import axios from 'axios'
......@@ -104,7 +104,7 @@ export default {
filters = defaults(filters, {providers: [], retributeOnly: true})
return {
sources: sources.sources,
maxDays: 60,
maxDays: this.$store.state.cache.maxDays || 60,
isLoadingSources: false,
isLoadingRetribute: false,
results: {},
......@@ -171,9 +171,11 @@ export default {
Object.keys(r.results.accounts).forEach((key) => {
if (aggregated[key]) {
aggregated[key].weight += r.results.accounts[key].weight
merge(aggregated[key].detail,r.results.accounts[key].detail)
aggregated[key].accounts.push(account)
} else {
aggregated[key] = {...r.results.accounts[key], accounts: [account], fullId: key}
aggregated[key] = {
...r.results.accounts[key], accounts: [account], fullId: key}
}
})
}
......@@ -234,7 +236,6 @@ export default {
await this.lookups(ids)
}
this.isLoadingRetribute = false
},
async lookups(ids) {
let self = this
......@@ -308,6 +309,12 @@ export default {
},
deep: true
},
"maxDays": {
handler (v) {
this.$store.commit('setRecursiveState', {key: 'cache.maxDays', value: this.maxDays})
},
deep: true
},
providers: {
handler (v) {
this.$store.commit('setRecursiveState', {key: 'cache.providers', value: this.providers})
......
import Mastodon from "./mastodon"
import Funkwhale from "./funkwhale"
import PeerTube from "./peertube"
import sortBy from "lodash/sortBy"
......@@ -12,6 +13,7 @@ export default {
sources: {
funkwhale: Funkwhale,
mastodon: Mastodon,
peertube: PeerTube,
musicbrainz: {
defaultAvatarIcon: "music_note",
connect: false
......
......@@ -116,14 +116,7 @@ export default {
}
handledFavorites += 1
results.progressCount += 1
let accountId
if (f.account.acct.indexOf('@') > -1) {
// the account has full id already
accountId = `webfinger:${f.account.acct}`
} else {
// it's probably a local account, we add the domain by hand
accountId = `webfinger:${f.account.acct}@${account.domain}`
}
let accountId = `activitypub:${f.account.url}`
if (results.accounts[accountId]) {
results.accounts[accountId].weight += 1
......@@ -133,8 +126,8 @@ export default {
} else {
results.accounts[accountId] = {
weight: 1,
source: 'webfinger',
id: f.account.acct,
source: 'activitypub',
id: f.account.url,
avatar: f.account.avatar,
name: f.account.display_name,
url: f.account.url,
......
import axios from 'axios'
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import moment from 'moment'
import Form from '../components/PeerTubeForm.vue'
import get from 'lodash/get'
import {getAbsoluteUrl} from "@/utils"
export const SCOPES = "user"
export async function createApp(ax) {
return await ax.get('/api/v1/oauth-clients/local')
}
function getAuthorizationHeader (account, state) {
const token = state.sources.peertube.appTokens[`${account.username}@${account.domain}`].access_token
return `Bearer ${token}`
}
export function getOauthAxios ({axiosParams, account, store}) {
// an axios instance with autorefreshing for oauth tokens
let client = axios.create(axiosParams || {})
client.interceptors.request.use(request => {
request.headers['Authorization'] = getAuthorizationHeader(account, store.state)
return request
})
createAuthRefreshInterceptor(client, (failedRequest) => {
console.log('Request failed, OAuth token probably expired, fetching a new one…')
const refreshToken = store.state.sources.peertube.appTokens[`${account.username}@${account.domain}`].refresh_token
const app = store.state.sources.peertube.appCredentials[account.domain]
const payload = {
client_id: app.client_id,
client_secret: app.client_secret,
grant_type: "refresh_token",
refresh_token: refreshToken,
}
return client.post(
`/api/v1/users/token`,
asForm(payload),
{headers: {'Content-Type': 'multipart/form-data'}}
).then((tokenRefreshResponse) => {
store.commit('setRecursiveState', {key: "sources.peertube.appTokens", suffix: `${account.username}@${account.domain}`, value: tokenRefreshResponse.data})
let newHeader = getAuthorizationHeader(account, store.state)
failedRequest.response.config.headers['Authorization'] = newHeader
return Promise.resolve()
})
})
return client
}
function asForm (obj) {
let data = new FormData()
Object.entries(obj).forEach((e) => {
data.set(e[0], e[1])
})
return data
}
function objectToQs (obj) {
let str = [];
for (let p in obj)
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
return str.join("&");
}
export async function connect({domain, router, store, username, password}) {
console.log(`Received connect callback for peertube domain ${domain}`)
console.log('Fetching token...')
const app = store.state.sources.peertube.appCredentials[domain]
const ax = axios.create()
const payload = {
client_id: app.client_id,
client_secret: app.client_secret,
grant_type: "password",
response_type: "code",
username,
password,
}
const response = await ax.post(
`https://${domain}/api/v1/users/token`,
objectToQs(payload),
{headers: {'Content-Type': 'application/x-www-form-urlencoded'}}
)
const token = response.data.access_token
const client = axios.create({
baseURL: `https://${domain}`,
headers: {'Authorization': `Bearer ${token}`},
})
const accountResponse = await client.get('/api/v1/users/me/')
const fullUsername = `${accountResponse.data.username}@${domain}`
store.commit('setRecursiveState', {key: "sources.peertube.appTokens", suffix: fullUsername, value: response.data})
store.commit('addAccount', {source: 'peertube', raw: accountResponse.data, username: accountResponse.data.username, domain})
router.push({path: '/'})
}
export default {
id: "peertube",
label: "PeerTube",
url: "https://joinpeertube.org",
description: "PeerTube servers",
extendedDescription: "Receive suggestions based on your viewing history",
imageBackground: "grey lighten-5",
defaultAvatarIcon: "ondemand_video",
form: Form,
connect: true,
getLogo () {
return require("../assets/sources/peertube-logo.svg")
},
authDataToKey ({username, domain}) {
return `peertube ${domain} ${username}`
},
getAvatar (data) {
if (!data.raw || !data.raw.account.avatar) {
return require("../assets/sources/peertube-logo.svg")
}
return data.raw.account.avatar.path
},
getDisplayName ({username, domain}) {
return `${username}@${domain}`
},
async fetch ({account, store, results, maxDays}) {
results.status = `Fetching views...`
const token = store.state.sources.peertube.appTokens[`${account.username}@${account.domain}`].access_token
const client = getOauthAxios({
axiosParams: {baseURL: `https://${account.domain}`},
store: store,
account: account
})
const dateLimit = moment().subtract(maxDays, 'days')
let url = null
let handledViews = 0
// results.progress = 0
// results.progressCount
results.accounts = {}
let cont = true
let start = 0
let perPage = 100
while (cont) {
let response = await client.get('/api/v1/users/me/history/videos', {params: {start, count: perPage}})
response.data.data.forEach((v) => {
let date = moment(v.publishedAt)
if (date.isBefore(dateLimit)) {
cont = false
return
}
handledViews += 1
results.progressCount += 1
let channel = v.channel
let channelId = `activitypub:${channel.url}`
if (results.accounts[channelId]) {
results.accounts[channelId].weight += 1
let detail = get(results.accounts[channelId], 'detail', {})
detail = get(detail, 'peertube', {views: 0})
detail.views += 1
} else {
results.accounts[channelId] = {
weight: 1,
source: 'peertube',
id: channelId,
avatar: (channel.avatar || {}).path,
name: channel.displayName,
url: channel.url,
detail: {
peertube: {
views: 1
}
}
}
}
// on PeerTube, both channels and accounts are AP actors
// and can have descriptions
let account = v.account
let accountId = `activitypub:${account.url}`
if (results.accounts[accountId]) {
results.accounts[accountId].weight += 1
let detail = get(results.accounts[accountId], 'detail', {})
detail = get(detail, 'peertube', {views: 0})
detail.views += 1
} else {
results.accounts[accountId] = {
weight: 1,
source: 'peertube',
id: accountId,
avatar: (account.avatar || {}).path,
name: account.displayName,
url: account.url,
detail: {
peertube: {
views: 1
}
}
}
}
// results.progress = Math.min(100, handledViews / maxFavorites * 100)
})
start += response.data.data.length
if (start >= response.data.total ) {
break
}
results.status = `Fetched ${handledViews} views`
}
results.isLoading = false
return results
},
getDetailMessage (detail) {
if (!detail) {
return
}
let views = detail.views || 0
return `peertube: ${views} views`
}
}
......@@ -37,4 +37,7 @@ export default {
.card .card-image {
padding: 1em;
}
.card-image img {
height: 5em;
}
</style>
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment