-
-
-
-

- M8SH -

-

- What web should have have received years ago. -

-

- Heavily extended fork of Gitea with integrated VPN, email, messenger, media streaming, gpg-auth and - more... -

-
-

M8SH is a decentralized network using the well-known paradigm of email addresses - for user identification — but offering much more. All powered by a decentralized network, with - the goal of replacing monopolized data markets and restoring our freedom to own, distribute, and - sell different forms of data. Rules of distribution and monetization should be decided by the - community - not a hegemon.

-
-
-
-
+
-
-
-

- 🔍 Search +
+

+ M8SH

-

- Each server has an integrated search engine that can search across other instances. Public content - accessible via regular websites. +

+ Work in progress +

+

+ A heavily extended Gitea fork - integrated VPN, email, messenger, media streaming, GPG-based auth, and more.

-
-

- 🔐 Pure Cryptography -

-

- No passwords. No sessions. No KYC. Keypair authentication with signature verification against your home - server. -

-
-

-
-
-

- 💬 Messenger & Cloud -

-

- Built-in messenger with end-to-end cryptographic guarantees. Content over 50 MB redistributed via - BitTorrent. +

+

+ M8SH is what email should have evolved into, but didn't. +

+

+ The naming convention stays - name@example.com - because that part was always right. + What email got right was federation: anyone could run a server and communicate freely, without depending on proprietary software. + What it got catastrophically wrong was everything else - cryptography, security, protocols, user experience. + Ray Tomlinson's invention served its purpose in the 70s. Then corporations arrived, offered convenience in exchange for privacy, + and the protocol was effectively abandoned. GPG extensions were proposed and ignored, because they were inconvenient for the corps. +

+

+ M8SH turns Gitea from a development platform into a complete content exchange and distribution system — + decentralized, no dozen separate accounts, no walled gardens. The one thing email got right — + user@domain — stays as the identity primitive, rebuilt on modern protocols and cryptography.

-
-

- 🎵 Multimedia -

-

- Full multimedia support: music, reels, videos, streams. Content is fingerprinted — copies are detected - and never shown. -

+ +
+

+ Roadmap +

+
    +
  1. + 01 + GPG-based registration, auth and authentication - decentralized by design +
  2. +
  3. + 02 + Integrated email server - for compatibility with legacy systems +
  4. +
  5. + 03 + Integrated messenger - end-to-end encrypted via GPG public keys +
  6. +
  7. + 04 + Federated search - indexed across M8SH nodes, accessible via API +
  8. +
  9. + 05 + Posts, articles, videos, reels, music +
  10. +
  11. + 06 + Integrated VPN +
  12. +
  13. + 07 + Encrypted cloud storage +
  14. +
+
{{template "base/footer" .}} \ No newline at end of file diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 76fbf3c159..fd9ff52ce0 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -129,11 +129,7 @@
{{if .Author}} {{ctx.AvatarUtils.Avatar .Author 20}} - {{if .Author.FullName}} - {{.Author.FullName}} - {{else}} - {{.Commit.Author.Name}} - {{end}} + {{.Author.GetShortDisplayNameLinkHTML}} {{else}} {{ctx.AvatarUtils.AvatarByEmail .Commit.Author.Email .Commit.Author.Email 20}} {{.Commit.Author.Name}} @@ -141,18 +137,19 @@
{{DateUtils.TimeSince .Commit.Committer.When}} -
- {{if or (ne .Commit.Committer.Name .Commit.Author.Name) (ne .Commit.Committer.Email .Commit.Author.Email)}} + {{$committerIsAuthor := and (eq .Commit.Committer.Name .Commit.Author.Name) (eq .Commit.Committer.Email .Commit.Author.Email)}} + {{if not $committerIsAuthor}} +
{{ctx.Locale.Tr "repo.diff.committed_by"}} - {{if and .Verification.CommittingUser .Verification.CommittingUser.ID}} + {{if and .Verification.CommittingUser}} {{ctx.AvatarUtils.Avatar .Verification.CommittingUser 20}} - {{.Commit.Committer.Name}} + {{.Verification.CommittingUser.GetShortDisplayNameLinkHTML}} {{else}} - {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Name 20}} + {{ctx.AvatarUtils.AvatarByEmail .Commit.Committer.Email .Commit.Committer.Email 20}} {{.Commit.Committer.Name}} {{end}} - {{end}} -
+
+ {{end}} {{if .CommitOtherParticipants}}
@@ -162,16 +159,12 @@ {{$gitIdentity := $participant.GitIdentity}} {{if $user}} {{ctx.AvatarUtils.Avatar $user 20}} - {{$user.GetDisplayName}} + {{$user.GetShortDisplayNameLinkHTML}} {{else}} {{$gitName := $gitIdentity.Name}} {{$gitEmail := $gitIdentity.Email}} - {{ctx.AvatarUtils.AvatarByEmail $gitEmail $gitName 20}} - {{if $gitEmail}} - {{$gitName}} - {{else}} - {{$gitName}} - {{end}} + {{ctx.AvatarUtils.AvatarByEmail $gitEmail $gitEmail 20}}{{/* use the same layout as the "author" above */}} + {{$gitName}} {{end}} {{end}}
diff --git a/tests/integration/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index cd87bc8b1a..d222aac362 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -638,7 +638,7 @@ jobs: return false } if latestCommitStatuses[0].State == commitstatus.CommitStatusPending { - insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) + insertFakeStatus(t, repo, sha, latestCommitStatuses[0]) return true } return false @@ -680,14 +680,18 @@ func checkCommitStatusAndInsertFakeStatus(t *testing.T, repo *repo_model.Reposit assert.Len(t, latestCommitStatuses, 1) assert.Equal(t, commitstatus.CommitStatusPending, latestCommitStatuses[0].State) - insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) + insertFakeStatus(t, repo, sha, latestCommitStatuses[0]) } -func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha, targetURL, context string) { +// insertFakeStatus inserts a success status that lands in the same dedupe +// group as `prev` — the actions runner mixes the workflow file path into +// ContextHash, so we must reuse it (rather than recomputing from Context). +func insertFakeStatus(t *testing.T, repo *repo_model.Repository, sha string, prev *git_model.CommitStatus) { err := commitstatus_service.CreateCommitStatus(t.Context(), repo, user_model.NewActionsUser(), sha, &git_model.CommitStatus{ - State: commitstatus.CommitStatusSuccess, - TargetURL: targetURL, - Context: context, + State: commitstatus.CommitStatusSuccess, + TargetURL: prev.TargetURL, + Context: prev.Context, + ContextHash: prev.ContextHash, }) assert.NoError(t, err) } @@ -822,7 +826,7 @@ jobs: return false } if latestCommitStatuses[0].State == commitstatus.CommitStatusPending { - insertFakeStatus(t, repo, sha, latestCommitStatuses[0].TargetURL, latestCommitStatuses[0].Context) + insertFakeStatus(t, repo, sha, latestCommitStatuses[0]) return true } return false diff --git a/tests/sqlite.ini.tmpl b/tests/sqlite.ini.tmpl index 95a1df283f..a12735e06d 100644 --- a/tests/sqlite.ini.tmpl +++ b/tests/sqlite.ini.tmpl @@ -5,6 +5,7 @@ RUN_MODE = prod [database] DB_TYPE = sqlite3 PATH = gitea-test.db +SQLITE_JOURNAL_MODE = WAL [indexer] REPO_INDEXER_ENABLED = true diff --git a/web_src/js/utils/dom.test.ts b/web_src/js/utils/dom.test.ts index 3edbe94ce4..a30b29bab2 100644 --- a/web_src/js/utils/dom.test.ts +++ b/web_src/js/utils/dom.test.ts @@ -9,6 +9,8 @@ import { test('createElementFromHTML', () => { expect(createElementFromHTML('foobar').outerHTML).toEqual('foobar'); expect(createElementFromHTML('foo').outerHTML).toEqual('foo'); + expect(createElementFromHTML('foo').outerHTML).toEqual('foo'); + expect(createElementFromHTML('').outerHTML).toEqual(''); }); test('createElementFromAttrs', () => { diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index d6823fc895..b18f33f33d 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -267,9 +267,14 @@ export function isElemVisible(el: HTMLElement): boolean { export function createElementFromHTML(htmlString: string): T { htmlString = htmlString.trim(); + const isLetter = (code: number) => (code >= 65 && code <= 90) || (code >= 97 && code <= 122); + const startsWithTag = (s: string, tag: string) => { + return s.startsWith('<') && + s.substring(1, 1 + tag.length).toLowerCase() === tag.toLowerCase() && + !isLetter(s[1 + tag.length].charCodeAt(0)); + }; // There is no way to create some elements without a proper parent, jQuery's approach: https://github.com/jquery/jquery/blob/main/src/manipulation/wrapMap.js - // eslint-disable-next-line github/unescaped-html-literal - if (htmlString.startsWith('('tr')!; diff --git a/web_src/js/utils/url.ts b/web_src/js/utils/url.ts index 84328faf24..06e1aa2951 100644 --- a/web_src/js/utils/url.ts +++ b/web_src/js/utils/url.ts @@ -1,3 +1,5 @@ +import {html, htmlRaw} from './html.ts'; + export function urlQueryEscape(s: string) { // See "TestQueryEscape" in backend // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent#encoding_for_rfc3986 @@ -35,9 +37,9 @@ const urlLinkifyPattern = /(<([-\w]+)[^>]*>)|(<\/([-\w]+)[^>]*>)|(https?:\/\/[^\ const trailingPunctPattern = /[.,;:!?]+$/; // Convert URLs to clickable links in HTML, preserving existing HTML tags -export function linkifyURLs(html: string): string { +export function linkifyURLs(htmlString: string): string { let inAnchor = false; - return html.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => { + return htmlString.replace(urlLinkifyPattern, (match, _openTagFull, openTag, _closeTagFull, closeTag, url) => { // skip URLs inside existing tags if (openTag === 'a') { inAnchor = true; @@ -54,6 +56,6 @@ export function linkifyURLs(html: string): string { const cleanUrl = trailingPunct ? url.slice(0, -trailingPunct[0].length) : url; const trailing = trailingPunct ? trailingPunct[0] : ''; // safe because regexp only matches valid URLs (no quotes or angle brackets) - return `${cleanUrl}${trailing}`; // eslint-disable-line github/unescaped-html-literal + return html`${htmlRaw(cleanUrl)}${htmlRaw(trailing)}`; }); }