1
0
mirror of https://koodu.h-i.works/projects/thebadspace synced 2025-05-06 14:41:02 -05:00

Compare commits

...

47 Commits

Author SHA1 Message Date
RXP
d310cf4fb9 Merge pull request 'Refactor home search' (#21) from moiety/search into develop
Reviewed-on: https://koodu.h-i.works/projects/thebadspace/pulls/21
Reviewed-by: RXP <ro@noreply.koodu.h-i.works>
2025-05-05 22:44:21 +02:00
Zoë
fed8748923
refactor: rework the search form with CSS Grid 2025-05-05 22:40:59 +02:00
Zoë
04ac27ea04
fix: only transition form control colors
the blanket statement `all` causes some weird animations when resizing the page. setting this to only transition specific properties fixes this.
2025-05-05 22:40:59 +02:00
Zoë
4dafad447d
fix: set accessible name for search button
also changes the CSS for the text transform
2025-05-05 22:40:59 +02:00
Zoë
d165d0c2bd
feat: replace search icon with an SVG
this change allows us to use the font colour for the icon. it might also be nice to use a sprite.svg for all icons. this would use the same <use> mechanism as i did here.

this also optimises the SVG a bit
2025-05-05 22:40:59 +02:00
Zoë
3e070676e4
feat: add accessible form element focus indicator
adds a focus indicator that passes the accessibility requirements WCAG 2.2: §1.4.11 & §2.4.7
2025-05-05 22:40:59 +02:00
Zoë
c9c3b88f4d
style: apply editorconfig 2025-05-05 22:40:59 +02:00
ro
66b43b2d4e manual edit for pr #13
pr was old has a hell, so just made the change manually to avoid
updating a super old branch
2025-05-05 14:35:58 -06:00
ro
c2be80c005 removed CSV listing if not CSV present
when reading sources, CSV file were being listed as 'none' when going
through the loop and no file was present. commented that part out so
'none' is not being repeated
2025-05-05 14:29:25 -06:00
RXP
7cdd284968 Merge pull request 'fix: set alt text for the logo' (#17) from moiety/logo-alt into develop
Reviewed-on: https://koodu.h-i.works/projects/thebadspace/pulls/17
Reviewed-by: RXP <ro@noreply.koodu.h-i.works>
2025-05-05 22:27:32 +02:00
RXP
7f7f324163 Merge branch 'develop' into moiety/logo-alt 2025-05-05 22:27:21 +02:00
RXP
67d40bd166 Merge pull request 'fix: removes an errand <body>-tag' (#20) from moiety/body into develop
Reviewed-on: https://koodu.h-i.works/projects/thebadspace/pulls/20
Reviewed-by: RXP <ro@noreply.koodu.h-i.works>
2025-05-05 22:17:48 +02:00
RXP
aeaca3e0f0 Merge branch 'develop' into moiety/body 2025-05-05 22:17:34 +02:00
RXP
0ad5208638 Merge pull request 'Refactor: about page' (#23) from moiety/about into develop
Reviewed-on: https://koodu.h-i.works/projects/thebadspace/pulls/23
Reviewed-by: RXP <ro@noreply.koodu.h-i.works>
2025-05-05 22:13:35 +02:00
RXP
8ed702bd59 Merge branch 'develop' into moiety/about 2025-05-05 22:12:04 +02:00
RXP
a0fee9b6b1 Merge pull request 'Refactor index meta' (#22) from moiety/infex-meta into develop
Reviewed-on: https://koodu.h-i.works/projects/thebadspace/pulls/22
Reviewed-by: RXP <ro@noreply.koodu.h-i.works>
2025-05-05 22:08:45 +02:00
Zoë
cf11d246d1
refactor: current sources
changes:
- `<strong>` → `<h3>`
- add a `.strong` class to mimic the `<strong>` behaviour
- wrap lists in `<ul>`
- provide feedback when no sources are available
2025-05-02 20:58:16 +02:00
Zoë
2c88d45e52
style: make indentation consistent
also adds some HTML tags here and there but doesn’t change any content.
2025-05-02 20:37:34 +02:00
Zoë
5515503117
fix: set meta-index padding 2025-05-02 19:08:06 +02:00
Zoë
0f16b275a0
refactor: use a <table>-element for index-meta 2025-05-02 19:00:21 +02:00
Zoë
d138198305
feat: add visually-hidden utility 2025-05-02 18:54:51 +02:00
Zoë
c0e05c13e6
fix: removes an errand <body>-tag
also adds a new line at eof and trims some spaces
2025-05-02 00:50:00 +02:00
Zoë
dbea3ff219
fix: set alt text for the logo 2025-05-02 00:13:32 +02:00
ro
1420d61892 Updated README with set up instructions
Added requirments for the project, as well as setup/install and usage
information for people that want to take the project for a spin on their
own.
2025-04-23 19:22:26 -06:00
ro
bfce3c82ec create env after install
the creation of base .env file that stores global variables for the app
need to be automated, so it's created after composer install
automatically instead of issueing and additional command
2025-04-22 13:39:14 -06:00
ro
3e937c5083 Source creation and editing
A UI has been added for entering new sources and editing pre-existing
ones. No more having to edit the DB directly when wanting to add or
remove a source.
2025-04-18 15:16:32 -06:00
ro
b7e2bbb334 updated laravel, fixed composer
At some point the composer json got jacked, so that needed to be fixed
to update laravel to the latest.

also started putting in pieces for sources management through the UI
2025-04-17 23:47:16 -06:00
ro
56f445572f admin account set up
after the site is installed and the DB set up, there needed to be a way
to create the first account that will be used as the admin to access the
den, the admin section of tbs

the system makes a check to see if this account exists and if there
isn't one present, it shows the admin account set up screen on the
index. it goes away after the account is created.
2025-04-17 17:14:15 -06:00
ro
515de4c56b CSS fix for spacing issues in listing page
th spacing between location links on the listings UI was a bit funky to
made a quick tweak to smooth that out.
2025-04-16 18:23:11 -06:00
ro
73a0abf898 php cs fixer config fix
php formatting wasn't working due to some issues in the php cs fixer
config file. fixed
2025-04-16 15:04:32 -06:00
ro
07793a413a Cleaned up migrations, full text search in model
Migrations were pretty much useless for database set up, so that needed
some attention to make setting up the project easier. Now, all necessary
tables can be created by running the `artisan migrate` command assuming
one has the database parameters set in the .env file

also added full text search capabilites through the database set up,
which the model itself can use to find locations, so the search has been
updated in the appropriate locations as well.

still need to add initial account set up and a form for adding sources
through the UI, but this was a big step towards letting anyone set up
their own version of tbs
2025-04-15 13:58:12 -06:00
ro
8fbf927f2d just a quick push test for the new repo 2025-01-23 15:19:19 -06:00
ro
8ce9a7744a composer json tweak
composer json wasn't validating, so that needed to be fixed
2024-10-08 11:37:51 -06:00
ro
f7bd675b5d updated composer file; update versioning
a quick update to the composer file to have proper versioning and some
details about the project,

also updated the template to reflect the current version
2024-10-08 11:15:25 -06:00
ro
ccd0a7a3a9 added notes to locations, exports comments fix
locations needed another data point for notes to additional information
about instances that aren't covered by public comments, i.e. if the site
is still live, additional concerns, etc, so that's been added and the
appropriate template files added

also, public comments where not being included in the CSV exports, so that's
been patched as well
2024-10-04 14:04:49 -06:00
ro
80191a2e46 commented out old decription reference in loc repo
there was a reference to storing descriptions in the database that no
longer has a corresponding table since it was changed

just commenting it out for now because it will be implemented again at a
later date
2024-10-01 11:39:55 -06:00
ro
8353d154c4 DB edit: description to public_comments
the update process now compiles public comments from instance blocklists
and displays them, so the name of the table that stores these comments
has been changed so it's not confusing.

also made the appropriate changes on the backend and in the template
that shows that information
2024-09-29 16:34:52 -06:00
ro
098cd72186 form validation for member editing
added some form validation in the member controller to make sure all
necessary info is provided before editing/creating member
2024-09-29 16:11:50 -06:00
ro
31f45c4af5 add role checks for admin function
admin functions are not shown to member with incorrect roles, but added
a bit more padding in the controller itself to check if the role is
correct before running an admin action for a little extra security
2024-09-29 15:55:55 -06:00
ro
3c0762344e added member create ui
now that editing member work, that process needed to be fleshed out by
adding an admin method to add new members as well. now new members can
be added by an admin

also changed the name of a blade file that wasn't following the template
naming convention
2024-09-27 12:39:56 -06:00
ro
d0c8def297 added member edit admin ui
needed way to edit existing members info if needed, so simple ui was
created for admins of the site to change specific info, not including a
members avi or password.

also consolidated memeber action and ui into it's own controller for the
sake of clearer organization
2024-09-24 16:21:59 -06:00
ro
99e22f5697 added member profile editing
added a basic ui so logged in members can change their info/password
when needed
2024-09-20 15:09:45 -06:00
ro
2932af0d3f show multiple instance comments
the previous update cycle only showed the latest comment from Sources
concerning a specific instance

now all available comments are pulled from denylist data to be displayed
under Description on the front end
2024-09-17 15:41:31 -06:00
ro
0eeab6355e updated image uploading, edited html templates
changed the the way files are uploaded to go into their own directory
called 'references' organized by location, identified by uuid. the
'references' directory was added to git ignore to those images are not
saved in the repo, since every install will have their own set of images

also updated reference links to be shown on the front end if they have
been added to a location

unnecessary links where moved from the admin member template since they
have been incorporated into the appropriate area.

a template for editing member account information has also been added.
2024-09-13 15:12:43 -06:00
ro
43e0004ac5 Updated blocklist retrival process, template edits
The process for updating blocklists per source wasn't effecient, so it
has been rewritten to process each source individually before moving on,
relieving some processing load on the server and ensuring every source
comes back with data, even in the event it doesn't grab it the first
time.

also removed the recent list from the index page as the recently updated
list doesn't reflect what's been the last to get updated and changed the
theme color to match the current palette.
2024-09-12 19:20:13 -06:00
ro
d3d1d56680 upgraded to lastest Laravel version
codebase was a version behind the latest laravel version, so all those
dependencies have been update to bring it up to speed

also added a small tweak to fix a firefox specific bug with rendering
CSS files
2024-06-18 13:10:54 -06:00
ro
0a02a71983 mastdon csv export edits
the csv exports for mastodon had some formatting issues that was making
importing problematic, so those issues have been fixed so importing the
doc is easier
2024-05-16 16:07:36 -06:00
52 changed files with 3282 additions and 1868 deletions

2
.gitignore vendored
View File

@ -4,6 +4,8 @@
/public/hot
/public/storage
/public/reference
/public/assets/images/references
/public/assets/images/members
/storage/*.key
/vendor
.env

View File

@ -55,20 +55,15 @@ return $config
]
],
'no_multiline_whitespace_around_double_arrow' => true,
'no_spaces_around_offset' => true,
'no_unused_imports' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
'object_operator_without_whitespace' => true,
'single_blank_line_before_namespace' => true,
'ternary_operator_spaces' => true,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
'single_line_after_imports' => true,
'ordered_imports' => [
'sort_algorithm' => 'none',
],
//'single_blank_line_before_namespace' => true, php fixer doesn't like this rule?
//Other rules here...
])
->setLineEnding("\n");

View File

@ -1,9 +1,91 @@
# The Bad Space
A searcable catalog of the worst places on the web.
As the fediverse is a largely unregulated space, The Bad Space arose from a need for collaborative safety tools that can be used identify instances that house bad actors, are poorly moderated, and/or contain inappropriate/offensive content (CSAM, hate speech, fascist ideology, etc.) that puts marginalized communities at risk for harassment and abuse.
More features incoming
The goal of TBS is to provide a way for communities to work together based on their specific needs and not a one-size-fits-all moderation approach for a more tailored and nuanced experience.
An Hi Project =)
## Requirements
TBS is built with the [Laravel](https://laravel.com/) framework, so PHP 8.2 and [Composer](https://getcomposer.org/doc/00-intro.md) is required, as well as [PostgreSQL](https://www.postgresql.org/download/) 14+ for the database. [Git](https://git-scm.com/downloads) is not strictly required as the code base can be manually downloaded, but it does make installing and updating easier.
*NOTE*: On Linux, some additional PHP extensions need to be installed, but don't worry it's easy. Run `sudo apt-get install php-mbstring`, `sudo apt-get install php-xml` and `sudo apt-get install php-zip `to get them and you're good to go.
*NOTE*: Environment set up on a local machine can be a pain in the ass, so to make that easier [Herd](https://herd.laravel.com/) can be used for MacOS and Windows and [FlyEnv](https://www.macphpstudy.com/) for Linux folks that don't want to do it all by hand. [DBNgin](https://dbngin.com/) can be used to make database setup painless. Yes, and they are all free.
## Install
Use `git clone https://koodu.h-i.works/projects/thebadspace.git` to install or just go grab the zip from the repo and unzip it.
Create your database in PostgreSQL and remember the name of the database and the username and password used to access it.
Go into the folder through your terminal and run `composer install` to grab all the dependencies needed and create the `.env` file that stores all project specific variables.
Open the `.env` file and add your database information so it resembles the following:
```
DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=your_database_name
DB_USERNAME=database_username
DB_PASSWORD=database_password
```
Now that the app is aware of what database is being used, it's time to set up the tables.
Back in your terminal, run `php artisan migrate` and if your database is set up correctly, all the tables will be created automatically.
Congrats, you're all set up.
## Usage
TBS can be run locally or on a remote server.
To get up and running on your local machine, hop back in your terminal and run `php -S localhost:8000 -t public/ ` in the folder of the project.
If you're running it on a server, use these configs to get going.
**For Nginx**
```
server {
listen 80;
server_name yourcoolassdomain.com;
client_max_body_size 20M //Change to whatever to limit/increase file upload size
location / {
try_files $uri /index.php$is_args$args;
}
}
```
**For Apache**
```
<VirtualHost *:80>
ServerAdmin admin@yourcoolassdomain.com
ServerName yourcoolassdomain.com
ServerAlias www.yourcoolassdomain.com
DocumentRoot /path-to-fipamo-folder/public
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
</VirtualHost>
```
Remember to restart Apache/NGINX once these configs have been plugged in.
Go the root domain of the site you just set up (or http://localhost:8000 if running locally) and you'll see a screen to set up an admin account.
Once the admin account has been created, go to The Den section in the menu and login in.
### Adding Sources
The core of TBS is adding sources of the instances (currently just Mastodon, but more coming soon) to be used to compile exportable block lists. *Note:* While many instance block lists are publicly available, it is good practice to ask for permission to use them.
Sources can be added under the `Manage Sources` link. Fill in the appropriate information and then save. *Note:* Some instances keep their block lists private for safety reasons, so you will need an access token to use them. These tokens cannot be created by TBS and must be obtained from the instance itself.
Once everything is filled in, save the source.
To remove a source from the compilation process, mark its status as active and save.
## Compiling Sources
Now it's time to grab the block list data from the individual sources entered needed to compile curation information.
This can be done under `Manage Locations` on the Den front page. Click `Update Source Data` to grab block list info from the sources added earlier.
When that is completed, then click `Update Locations Data` to compile individual sources data into one that the site uses to display info about moderated instances. If this is being run for the first time, this must be clicked *twice*.
Once that's done, go to the main domain again, and you'll see the results of the compilation process.

View File

@ -34,14 +34,6 @@ class DenController extends Controller
]);
}
public function member(Request $request)
{
$member = Auth::user();
return view('back.member', [
'handle' => $member->handle,
'title' => "Manage Members"]);
}
public function locations(Request $request)
{
$member = Auth::user();

View File

@ -46,12 +46,12 @@ class ExportController extends Controller
$sources = Source::where("active", true)->get();
if ($type == 'mastodon') {
$columns = [
'domain',
'severity',
'public_comment',
'reject_media',
'reject_reports',
'obfuscate',
'#domain',
'#severity',
'#public_comment',
'#reject_media',
'#reject_reports',
'#obfuscate',
];
};
@ -60,13 +60,18 @@ class ExportController extends Controller
if ($rate * 100 >= $percent) {
if ($type == 'mastodon') {
//comman break teh CSV so just take them out
$comments = str_replace(",", ";", $location->description);
$comments = str_replace(",", ";", $location->public_comments);
//remove extra white space
$comments = str_replace(["\n\r", "\n", "\r"], " ", $comments);
$comments = str_replace(['"', "'"], "", $comments);
if ($location->rating == 'defederate') {
$rating = 'suspend';
} else {
$rating = $location->rating;
}
//add to the export list
array_push($list, [$location->url, $location->rating, $comments, "FALSE", "FALSE", "FALSE"]);
array_push($list, [$location->url, $rating, $comments, "FALSE", "FALSE", "FALSE"]);
}
}
}

View File

@ -6,33 +6,49 @@ use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Repositories\LocationRepository;
use App\Repositories\SourceRepository;
use App\Repositories\MemberRepository;
use App\Services\PaginationService;
class FrontIndexController extends Controller
{
protected $location;
protected $source;
protected $member;
protected $pagination;
public function __construct(
LocationRepository $locationRepository,
SourceRepository $sourceRepository,
MemberRepository $memberRepository,
PaginationService $paginationService
) {
$this->location = $locationRepository;
$this->source = $sourceRepository;
$this->member = $memberRepository;
$this->pagination = $paginationService;
}
public function start()
{
return view('front.index', [
'count' => count($this->location->getActiveLocations()),
'sources' => count($this->source->getActive()),
'recent' => $this->location->getRecent(),
'latest_date' => $this->location->getRecent()[0]->updated_at->format('Y M d'),
'title' => "The Bad Space"
]);
//check to see if there are any accounts
if (count($this->member->getAll()) == 0) {
return view('back.member', [
'mode' => 'admin-create',
'title' => "Welcome, welcome"]);
} else {
//for fresh installs that dont have any source data yet
$latest_update = 'Never Run';
if (count($this->location->getRecent()) != 0) {
$latest_update = $this->location->getRecent()[0]->updated_at->format('Y M d');
}
return view('front.index', [
'count' => count($this->location->getActiveLocations()),
'sources' => count($this->source->getActive()),
'recent' => $this->location->getRecent(),
'latest_date' => $latest_update,
'title' => "The Bad Space"
]);
}
}
public function indexSearch(Request $request)
@ -76,15 +92,19 @@ class FrontIndexController extends Controller
}
if (isset($member->role)) {
($member->role == 1 || $member->role == 2) ? $edit = true : $edit = false;
($member->role == 0 || $member->role == 1) ? $edit = true : $edit = false;
}
$links = explode(',', $location->archive_links);
$comments = explode('+', $location->public_comments);
return view('front.location', [
'title' => str_replace(".", " ", $name),
'location' => $location,
'comments' => $comments,
'actions' => $location->block_count + $location->silence_count,
'sources_count' => count($sources),
'images' => json_decode($location->images),
'links' => $links,
'updated' => $location->updated_at->format('Y M d'),
'edit' => $edit
]);

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Services\UpdateService;
use App\Repositories\LocationRepository;
use Illuminate\Support\Facades\Auth;
class LocationController extends Controller
{
@ -18,34 +19,51 @@ class LocationController extends Controller
$this->location = $locationRepository;
}
//actions
public function updateLocations()
{
$result = $this->update->data();
return back()->with(
'message',
$result
);
//role check
$member = Auth::user();
if ($member->role == 0) {
$result = $this->update->data();
return back()->with(
'message',
$result
);
} else {
return back()->withErrors('message', 'Nah, you don\'t have permission to do this');
}
}
public function compileLocations()
{
$result = $this->update->list();
return back()->with(
'message',
$result
);
//role check
$member = Auth::user();
if ($member->role == 0) {
$result = $this->update->list();
return back()->with(
'message',
$result
);
} else {
return back()->withErrors('message', 'Nah, you don\'t have permission to do this');
}
}
public function editLocation(Request $request)
{
$token = csrf_token();
$response = $this->location->editLocation($request);
if ($response['status']) {
return back()->with('message', $response['message']);
$token = csrf_token();
//role check
$member = Auth::user();
if ($member->role == 0 || $member->role == 1) {
$response = $this->location->editLocation($request);
if ($response['status']) {
return back()->with('message', $response['message']);
} else {
return back()->withErrors('message', $response['message']);
}
} else {
return back()->withErrors('message', $response['message']);
return back()->withErrors('message', 'Nah, you don\'t have permission to do this');
}
}
}

View File

@ -0,0 +1,182 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Repositories\MemberRepository;
class MemberController extends Controller
{
protected $member;
public function __construct(
MemberRepository $memberRepo
) {
$this->member = $memberRepo;
}
public function index(Request $request)
{
$member = Auth::user();
return view('back.member', [
'handle' => $member->handle,
'members' => $this->member->getAll(),
'mode' => 'index',
'title' => "Manage Members"]);
}
public function profile(Request $request)
{
$member = Auth::user();
$avi = '';
if ($member->avatar == 'default-member-avatar') {
$avi = '/assets/images/global/default-avi.png';
} else {
$avi = $member->avatar;
}
return view('back.profile', [
'title' => "Hey, it's you!",
'handle' => $member->handle,
'email' => $member->email,
'avatar' => $avi,
'pronouns' => $member->pronoun,
'uuid' => $member->uuid,
'role' => $member->role
]);
}
public function editMember(Request $request, $uuid = 0)
{
$member = $this->member->get($uuid);
$avi = '';
if ($member->avatar == 'default-member-avatar') {
$avi = '/assets/images/global/default-avi.png';
} else {
$avi = $member->avatar;
}
return view('back.member', [
'member' => $member,
'avatar' => $avi,
'mode' => 'member-edit',
'title' => "Edit Member Info"]);
}
public function createMember(Request $Request)
{
return view('back.member', [
'mode' => 'member-create',
'title' => "Make a new friend"]);
}
//actions
public function profileEdit(Request $request)
{
$token = csrf_token();
//check if logged in member id matches profile request id
$member = Auth::user();
if ($member->uuid == $request->id) {
//validate required fields
$valid = $request->validate([
'handle' => ['required'],
'email' => ['required'],
]);
if ($valid) {
$response = $this->member->editProfile($request);
if ($response['status'] == true) {
return back()->with('message', $response['message']);
} else {
return back()->withErrors([$response['message']]);
}
} else {
return back()->withErrors(['Misssing some required info, homie.']);
}
} else {
return back()->withErrors(['This is not your profile to edit.']);
}
}
public function memberEdit(Request $request)
{
$token = csrf_token();
//role check
$member = Auth::user();
if ($member->role == 0) {
$valid = $request->validate([
'handle' => ['required'],
'email' => ['required'],
'role' => ['required']
]);
if ($valid) {
$response = $this->member->edit($request);
if ($response['status'] == true) {
return back()->with('message', $response['message']);
} else {
return back()->withErrors([$response['message']]);
}
} else {
return back()->withErrors(['Misssing some required info, homie.']);
}
} else {
return back()->withErrors(['Nah, you can\'t do this. Wrong permissions.']);
}
}
public function memberCreate(Request $request)
{
$token = csrf_token();
$member = Auth::user();
if ($member->role == 0) {
$valid = $request->validate([
'handle' => ['required'],
'email' => ['required'],
'role' => ['required'],
'pronouns' => ['required'],
'fresh_pass' => ['required'],
'fresh_pass_confirm' => ['required'],
]);
if ($valid) {
$response = $this->member->add($request);
if ($response['status'] == true) {
return redirect('/den/member')->with('message', $response['message']);
} else {
return back()->withErrors([$response['message']]);
}
} else {
return back()->withErrors(['Misssing some required info, homie.']);
}
} else {
return back()->withErrors(['Nah, you can\'t do this. Wrong permissions.']);
}
}
public function adminCreate(Request $request)
{
//should only be run of no members exist
if (count($this->member->getAll()) == 0) {
$token = csrf_token();
$valid = $request->validate([
'handle' => ['required'],
'email' => ['required'],
'pronouns' => ['required'],
'fresh_pass' => ['required'],
'fresh_pass_confirm' => ['required'],
]);
if ($valid) {
$response = $this->member->add($request);
if ($response['status'] == true) {
return redirect('/den/member')->with('message', $response['message']);
} else {
return back()->withErrors([$response['message']]);
}
} else {
return back()->withErrors(['Misssing some required info, homie.']);
}
} else {
return back()->withErrors(['Shame on you for even trying that.']);
}
}
}

View File

@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Repositories\SourceRepository;
class SourceController extends Controller
{
protected $sources;
public function __construct(
SourceRepository $sourceRepo
) {
$this->sources = $sourceRepo;
}
public function index(Request $request)
{
$member = Auth::user();
return view('back.sources', [
'mode' => 'index',
'handle' => $member->handle,
'sources' => $this->sources->getAll(),
'title' => "Manage Sources"]);
}
public function editSource(Request $request, $id = 0)
{
$source = $this->sources->get($id);
return view('back.sources', [
'mode' => 'source-edit',
'source' => $source,
'title' => "Edit Source Info"]);
}
public function createSource(Request $Request)
{
return view('back.sources', [
'mode' => 'source-create',
'title' => "Enter a new Source"]);
}
//actions
public function sourceEdit(Request $request)
{
$token = csrf_token();
//role check
$member = Auth::user();
if ($member->role == 0) {
$valid = $request->validate([
'url' => ['required'],
'type' => ['required'],
'status' => ['required'],
'format' => ['required'],
]);
if ($valid) {
$response = $this->sources->edit($request);
if ($response['status'] == true) {
return back()->with('message', $response['message']);
} else {
return back()->withErrors([$response['message']]);
}
} else {
return back()->withErrors(['Misssing some required info, homie.']);
}
} else {
return back()->withErrors(['Nah, you can\'t do this. Wrong permissions.']);
}
}
public function sourceCreate(Request $request)
{
$token = csrf_token();
$member = Auth::user();
if ($member->role == 0) {
$valid = $request->validate([
'url' => ['required'],
'type' => ['required'],
'status' => ['required'],
'format' => ['required'],
]);
if ($valid) {
$response = $this->sources->add($request);
if ($response['status'] == true) {
return redirect('/den/sources')->with('message', $response['message']);
} else {
return back()->withErrors([$response['message']]);
}
} else {
return back()->withErrors(['Misssing some required info, homie.']);
}
} else {
return back()->withErrors(['Nah, you can\'t do this. Wrong permissions.']);
}
}
}

View File

@ -24,7 +24,7 @@ class Location extends Model
"uuid",
"name",
"url",
"description",
"public_comments",
"images",
"active",
"rating",
@ -35,6 +35,16 @@ class Location extends Model
"created_at",
"updated_at",
"actions_count",
"archive_links"
"archive_links",
"notes",
];
public function scopeSearch($query, $search)
{
if (!$search) {
return $query;
}
return $query->whereRaw('searchtext @@ to_tsquery(\'english\', ?)', [$search])
->orderByRaw('ts_rank(searchtext, to_tsquery(\'english\', ?)) DESC', [$search]);
}
}

View File

@ -9,6 +9,20 @@ class Member extends Authenticatable
{
use HasFactory;
protected $table = "member";
protected $fillable = ["uuid", "handle", "email", "password", "active", "role", "avatar", "pronoun", "gender"];
public $timestamps = false;
protected $table = "member";
protected $primaryKey = 'id';
public $incrementing = true;
protected $fillable = [
"uuid",
"handle",
"email",
"password",
"active",
"role",
"avatar",
"pronoun",
"created_at",
"last_login"
];
}

View File

@ -5,10 +5,12 @@ namespace App\Providers;
use Illuminate\Support\ServiceProvider;
use App\Repositories\LocationRepository;
use App\Repositories\SourceRepository;
use App\Repositories\MemberRepository;
use App\Services\UpdateService;
use App\Services\MaintenanceService;
use App\Models\Location;
use App\Models\Source;
use App\Models\Member;
class AppServiceProvider extends ServiceProvider
{
@ -25,6 +27,10 @@ class AppServiceProvider extends ServiceProvider
return new SourceRepository(new Source());
});
$this->app->bind(MemberRepository::class, function ($app) {
return new MemberRepository(new Member());
});
$this->app->bind(UpdateService::class, function ($app) {
return new UpdateService(
new LocationRepository(new Location()),

View File

@ -23,7 +23,8 @@ class LocationRepository
$rawSearch = $terms;
$terms = str_replace(",", "", $terms);
$terms = str_replace(" ", "|", $terms);
$raw = DB::select("SELECT * FROM searchlocations(?)", [$terms]);
//$raw = DB::select("SELECT * FROM searchlocations(?)", [$terms]);
$raw = Location::search($terms)->get();
$results = [];
foreach ($raw as $item) {
if (($item->block_count + $item->silence_count) >= 2) {
@ -56,24 +57,34 @@ class LocationRepository
public function editLocation($request)
{
$location = $this->getLocation($request->id);
$images = [];
$location = $this->getLocation($request->id);
$publicPath = '../public/';
$refPath = 'assets/images/references/' . $location->uuid;
$images = [];
if ($request->hasfile("references")) {
foreach ($request->references as $file) {
$path = $file->store('reference');
array_push($images, ["path" => $path]);
if (!is_dir($publicPath . $refPath)) {
mkdir($publicPath . $refPath, 0755, true);
}
$filename = urlencode($file->getClientOriginalName());
$file->move($publicPath . $refPath, $filename);
//$path = $file->store('reference');
array_push($images, ["path" => '/' . $refPath . '/' . $filename]);
}
}
$request->merge(['images' => json_encode($images)]);
if (!empty($images)) {
$request->merge(['images' => json_encode($images)]);
$location->images = json_encode($images);
}
$location->name = $request->name;
$location->description = $request->description;
$location->notes = $request->notes;
$location->archive_links = $request->archive_links;
$location->images = json_encode($images);
$result = [];
if ($location->save()) {
return ['status' => true, 'message' => "Location Editited -IMG- " . $request->hasfile("references")];
return ['status' => true, 'message' => "Location Editited" . $request->hasfile("references")];
} else {
return ['status' => false, 'message' => "Location Not Editited"];
}

View File

@ -0,0 +1,145 @@
<?php
namespace App\Repositories;
use App\Models\Member;
use Illuminate\Support\Facades\Hash;
use Ramsey\Uuid\Uuid;
use Carbon\Carbon;
class MemberRepository
{
protected $model;
public function __construct(Member $model)
{
$this->model = $model;
}
public function getAll()
{
return $this->model::all();
}
public function get($uuid)
{
return $this->model::where("uuid", $uuid)->first();
}
public function edit($request)
{
//get member to edit
$member = $this->get($request->id);
//save new avi if available
$publicPath = '../public/';
$refPath = 'assets/images/members/' . $member->uuid;
if ($request->hasfile("fresh_avi")) {
$file = $request->fresh_avi;
if (!is_dir($publicPath . $refPath)) {
mkdir($publicPath . $refPath, 0755, true);
}
$filename = urlencode($file->getClientOriginalName());
$file->move($publicPath . $refPath, $filename);
$freshAvi = '/' . $refPath . '/' . $filename;
$member->avatar = $freshAvi;
}
$member->handle = $request->handle;
$member->email = $request->email;
$member->pronoun = $request->pronouns;
$member->role = $request->role;
$member->active = $request->status;
if ($member->save()) {
return ['status' => true, 'message' => "Member Editited"];
} else {
return ['status' => false, 'message' => "Member Not Editited"];
}
}
public function add($request)
{
$password = [];
$newFriend = [];
if ($request->fresh_pass === $request->fresh_pass_confirm) {
$password = Hash::make($request->fresh_pass);
} else {
return ['status' => false, 'message' => "Passwords Do Not Match"];
}
//if role paramter is set, not an admin add
if (isset($request->role)) {
$newFriend = $this->model::create([
'uuid' => Uuid::uuid4(),
'avatar' => 'default-member-avatar',
'handle' => $request->handle,
'email' => $request->email,
'pronoun' => $request->pronouns,
'role' => $request->role,
'active' => $request->status,
'password' => $password,
'created_at' => Carbon::now(),
'last_login' => Carbon::now(),
]);
} else {
//set up admin
$newFriend = $this->model::create([
'uuid' => Uuid::uuid4(),
'avatar' => 'default-member-avatar',
'handle' => $request->handle,
'email' => $request->email,
'pronoun' => $request->pronouns,
'role' => 0,
'active' => true,
'password' => $password,
'created_at' => Carbon::now(),
'last_login' => Carbon::now(),
]);
}
if ($newFriend) {
return ['status' => true, 'message' => "New Friend Made!"];
} else {
return ['status' => false, 'message' => "Uh oh, New Friend Delay!"];
}
}
public function editProfile($request)
{
//get member to edit
$member = $this->get($request->id);
//save new avi if available
$publicPath = '../public/';
$refPath = 'assets/images/members/' . $member->uuid;
if ($request->hasfile("fresh_avi")) {
$file = $request->fresh_avi;
if (!is_dir($publicPath . $refPath)) {
mkdir($publicPath . $refPath, 0755, true);
}
$filename = urlencode($file->getClientOriginalName());
$file->move($publicPath . $refPath, $filename);
$freshAvi = '/' . $refPath . '/' . $filename;
$member->avatar = $freshAvi;
}
//changing password
if (isset($request->fresh_pass) && $request->fresh_pass !== '') {
if ($request->fresh_pass === $request->fresh_pass_confirm) {
$member->password = Hash::make($request->fresh_pass);
} else {
return ['status' => false, 'message' => "Passwords Do Not Match"];
}
}
$member->handle = $request->handle;
$member->email = $request->email;
$member->pronoun = $request->pronouns;
if ($member->save()) {
return ['status' => true, 'message' => "Profile Editited"];
} else {
return ['status' => false, 'message' => "Profile Not Editited"];
}
}
}

View File

@ -9,10 +9,26 @@ use GuzzleHttp\Exception\ConnectException;
class SourceRepository
{
protected $source;
protected $missing;
protected $updated;
protected $sources;
public function __construct(Source $source)
{
$this->source = $source;
$this->source = $source;
$this->missing = [];
$this->updated = [];
$this->sources = $source::where("active", true)->get();
}
public function getAll()
{
return $this->source::all();
}
public function get($id)
{
return $this->source::where("id", $id)->first();
}
public function getActive()
@ -20,51 +36,120 @@ class SourceRepository
return $this->source::where("active", true)->get();
}
public function updateSourceData()
public function edit($request)
{
$source = $this->get($request->id);
$source->url = $request->url;
$source->type = $request->type;
$source->format = $request->format;
$source->active = $request->status;
$source->last_updated = Carbon::now();
//token check
if ($request->token != '' && $request->token != 'none') {
$source->token = $request->token;
} else {
$source->token = '';
}
if ($source->save()) {
return ['status' => true, 'message' => "Source Editited"];
} else {
return ['status' => false, 'message' => "Source Not Editited"];
}
}
public function add($request)
{
$newSource = $this->source::create([
'url' => $request->url,
'type' => $request->type,
'format' => $request->format,
'active' => $request->status,
'token' => $request->token,
'last_udated' => Carbon::now(),
]);
if ($newSource) {
return ['status' => true, 'message' => "New Source Created!"];
} else {
return ['status' => false, 'message' => "Uh oh, New Source Not Created!"];
}
}
public function updateSourceData($index = 0)
{
$sources = $this->getActive();
$missing = [];
$checked = [];
//checks all the sources to refresh data
foreach ($sources as $source) {
$result = [];
if ($source['type'] == 'mastodon') {
if ($source['token'] == null) {
try {
$result = \Mastodon::domain('https://' . $source['url'])
->get('/instance/domain_blocks');
array_push($checked, ['source' => $source->url]);
} catch (ConnectException $e) {
array_push($missing, ['source' => $source->url]);
}
$count = count($this->sources);
if ($count == 0) {
return [
'checked' => $this->updated,
'notchecked' => $this->missing,
'count' => $count,
'updated' => 'false',
];
} else {
//check index
if ($index <= $count - 1) {
$source = $this->sources[$index];
$result = $this->getMastoBlocklist($source);
if (count($result) > 0) {
$source->list_data = json_encode($result);
$source->last_updated = Carbon::now();
$source->save();
array_push($this->updated, ['source' => $source->url]);
$index++;
$result = $this->updateSourceData($index);
} else {
try {
$result = \Mastodon::domain('https://' . $source['url'])
->token($source['token'])
->get('/instance/domain_blocks');
array_push($checked, ['source' => $source->url]);
} catch (ConnectException $e) {
}
//if empty run the same index again
array_push($this->missing, ['source' => $source->url]);
$result = $this->updateSourceData($index);
}
} elseif ($source['type'] == 'custom' && $source['format'] == 'csv') {
} else {
//continue
}
return [
'checked' => $this->updated,
'notchecked' => $this->missing,
'count' => $count,
'updated' => 'true',
];
}
}
private function getMastoBlocklist($source)
{
$result = [];
if ($source['type'] == 'mastodon') {
if ($source['token'] == null) {
try {
$denylist = array_map('str_getcsv', file('https://' . $source['url']));
foreach ($denylist as $item) {
array_push($result, [
'domain' => $item[0],
'severity' => $item[1],
'comment' => $item[2]]);
}
array_push($checked, ['source' => $source->url]);
} catch (Exception $e) {
array_push($missing, ['source' => $source->url]);
$result = \Mastodon::domain('https://' . $source['url'])
->get('/instance/domain_blocks');
} catch (ConnectException $e) {
//TODO: Logo Errors
}
} else {
try {
$result = \Mastodon::domain('https://' . $source['url'])
->token($source['token'])
->get('/instance/domain_blocks');
} catch (ConnectException $e) {
//TODO: Logo Errors
}
}
$source->list_data = json_encode($result);
$source->last_updated = Carbon::now();
$source->save();
} elseif ($source['type'] == 'custom' && $source['format'] == 'csv') {
try {
$denylist = array_map('str_getcsv', file('https://' . $source['url']));
foreach ($denylist as $item) {
array_push($result, [
'domain' => $item[0],
'severity' => $item[1],
'comment' => $item[2]]);
}
array_push($checked, ['source' => $source->url]);
} catch (Exception $e) {
array_push($missing, ['source' => $source->url]);
}
}
return ['checked' => $checked, 'notchecked' => $missing];
return $result;
}
}

View File

@ -23,8 +23,11 @@ class UpdateService
public function data()
{
$response = $this->source->updateSourceData();
return count($response['checked']) . ' SOURCES UPDATED - ' .
count($response['notchecked']) . ' SOURCES NOT CHECKED';
if ($response['updated'] == 'true') {
return count($response['checked']) . ' SOURCES UPDATED';
} else {
return 'NO SOURCES PRESENT';
}
}
public function list()
@ -33,14 +36,15 @@ class UpdateService
$fresh = 0;
$unified = [];
$sources = $this->source->getActive();
$sources = $this->source->getActive();
$locations = $this->location->getActiveLocations();
foreach ($sources as $source) {
//$listData = json_decode();
foreach (json_decode($source->list_data) as $item) {
$index = array_search($item->domain, array_column($unified, 'url'));
if ($index) {
//if there is a match, update the count
//if there is a match, update the count and comment
if ($item->severity == "suspend" || $item->severity == "defederate") {
++$unified[$index]['block_count'];
array_push($unified[$index]['block_vote'], $source->url);
@ -48,6 +52,9 @@ class UpdateService
++$unified[$index]['silence_count'];
array_push($unified[$index]['silence_vote'], $source->url);
}
if (!is_null($item->comment) && $item->comment != ' ' && $item->comment != '') {
$unified[$index]['comment'] = $item->comment . '+' . $unified[$index]['comment'];
}
} else {
$silence = 0;
$suspend = 0;
@ -74,6 +81,12 @@ class UpdateService
}
}
//clear out all previous descriptions
foreach ($locations as $loc) {
$loc->public_comments = ' ';
$loc->save();
}
foreach ($unified as $item) {
$location = $this->location->getLocation($item['url']);
if ($location) {
@ -86,6 +99,12 @@ class UpdateService
$location->silence_count = $item['silence_count'];
$location->silence_vote = [];
$location->silence_vote = $item['silence_vote'];
//clear descriptions
if (!is_null($item['comment']) || !$item['comment'] != " ") {
$location->public_comments = $item['comment'];
} else {
$location->public_comments = 'Comments Pending';
}
$location->actions_count = $item['block_count'] + $item['silence_count'];
@ -112,20 +131,20 @@ class UpdateService
}
$new = Location::create([
'uuid' => Uuid::uuid4(),
'name' => $item['url'],
'url' => $item['url'],
'description' => ($item['comment'] != null) ? $item['comment'] : "no description",
'active' => $status,
'rating' => $rating,
'added_by' => 1,
'tags' => 'poor moderation, hate speech',
'images' => json_encode($images),
'block_count' => $item['block_count'],
'block_vote' => $item['block_vote'],
'silence_count' => $item['silence_count'],
'silence_vote' => $item['silence_vote'],
'actions_cont' => $item['block_count'] + $item['silence_count']
'uuid' => Uuid::uuid4(),
'name' => $item['url'],
'url' => $item['url'],
'public_comments' => ($item['comment'] != null) ? $item['comment'] : "comments pending",
'active' => $status,
'rating' => $rating,
'added_by' => 1,
'tags' => 'poor moderation, hate speech',
'images' => json_encode($images),
'block_count' => $item['block_count'],
'block_vote' => $item['block_vote'],
'silence_count' => $item['silence_count'],
'silence_vote' => $item['silence_vote'],
'actions_cont' => $item['block_count'] + $item['silence_count']
]);
}
}

View File

@ -1,14 +1,39 @@
{
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": ["laravel", "framework"],
"license": "MIT",
"name": "project/thebadspace",
"type": "moderation",
"description": "A tool for improving independent social media curation",
"version": "0.7-alpha",
"keywords": [
"thebadspace",
"tbs",
"activty-pub",
"laravel",
"framework",
"moderation",
"safety",
"curation",
"tooling",
"fediverse"
],
"license": [
"GPL-3.0-only"
],
"authors": [
{
"name": "Ro",
"homepage": "https://roiskinda.cool"
}
],
"support": {
"source": "https://koodu.h-i.works/projects/thebadspace",
"wiki": "https://koodu.h-i.works/projects/thebadspace/wiki/?action=_pages",
"issues": "https://koodu.h-i.works/projects/thebadspace/issues"
},
"require": {
"php": "^8.1",
"php": "^8.2",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/sanctum": "^3.2",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.0",
"laravel/tinker": "^2.8",
"revolution/laravel-mastodon-api": "^3.0"
},
@ -17,8 +42,8 @@
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
"mockery/mockery": "^1.4.4",
"nunomaduro/collision": "^7.0",
"phpunit/phpunit": "^10.1",
"nunomaduro/collision": "^8.1",
"phpunit/phpunit": "^11.0",
"spatie/laravel-ignition": "^2.0"
},
"autoload": {
@ -46,11 +71,17 @@
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi"
]
],
"post-install-cmd": [
"php -r \"copy('.env.example', '.env');\"",
"php artisan key:generate"
]
},
"extra": {
"laravel": {
"dont-discover": []
"dont-discover": [
]
}
},
"config": {

2787
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('users');
}
};

View File

@ -1,28 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('password_reset_tokens');
}
};

View File

@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('failed_jobs', function (Blueprint $table) {
$table->id();
$table->string('uuid')->unique();
$table->text('connection');
$table->text('queue');
$table->longText('payload');
$table->longText('exception');
$table->timestamp('failed_at')->useCurrent();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('failed_jobs');
}
};

View File

@ -1,33 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('personal_access_tokens', function (Blueprint $table) {
$table->id();
$table->morphs('tokenable');
$table->string('name');
$table->string('token', 64)->unique();
$table->text('abilities')->nullable();
$table->timestamp('last_used_at')->nullable();
$table->timestamp('expires_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('personal_access_tokens');
}
};

View File

@ -0,0 +1,57 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('location', function (Blueprint $table) {
$table->bigIncrements('id');
$table->uuid('uuid');
$table->string('name', length: 255);
$table->string('url', length: 255);
$table->text('public_comments');
$table->json('images')->nullable();
$table->boolean('active');
$table->string('rating', length: 255);
$table->integer('added_by');
$table->timestamps(precision: 0);
$table->timestamp('deleted_at', precision: 0)->nullable();
$table->string('tags', length: 255);
$table->integer('block_count')->nullable();
$table->integer('silence_count')->nullable();
$table->integer('actions_count')->nullable();
$table->text('archive_links')->nullable();
$table->json('block_vote')->nullable();
$table->json('silence_vote')->nullable();
$table->text('notes')->nullable();
});
DB::statement("ALTER TABLE location ADD COLUMN searchtext TSVECTOR");
DB::statement("UPDATE location SET searchtext = to_tsvector('english', name)");
DB::statement("UPDATE location SET searchtext = to_tsvector('english', url)");
DB::statement("UPDATE location SET searchtext = to_tsvector('english', public_comments)");
DB::statement("UPDATE location SET searchtext = to_tsvector('english', tags)");
DB::statement("CREATE INDEX searchtext_gin ON location USING GIN(searchtext)");
DB::statement("CREATE TRIGGER ts_searchtext BEFORE INSERT OR UPDATE ON location FOR EACH ROW EXECUTE PROCEDURE tsvector_update_trigger('searchtext', 'pg_catalog.english', 'name', 'url', 'public_comments', 'tags')");
}
//'name', 'url', 'public_comments', 'tags'
/**
* Reverse the migrations.
*/
public function down(): void
{
DB::statement("DROP TRIGGER IF EXISTS tsvector_update_trigger ON location");
DB::statement("DROP INDEX IF EXISTS searchtext_gin");
DB::statement("ALTER TABLE location DROP COLUMN searchtext");
Schema::dropIfExists('location');
}
};

View File

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('member', function (Blueprint $table) {
$table->bigIncrements('id');
$table->uuid('uuid');
$table->string('handle', length: 255);
$table->string('email', length: 255);
$table->string('password', length: 255);
$table->string('avatar', length: 255)->nullable();
$table->string('pronoun', length: 255);
$table->string('gender', length: 255)->nullable();
$table->boolean('active');
$table->integer('role')->nullable();
$table->timestamp('created_at', precision: 0);
$table->timestamp('last_login', precision: 0);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('member');
}
};

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('source', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('url', length: 255);
$table->string('type', length: 255);
$table->boolean('active');
$table->integer('admin_id')->nullable();
$table->string('format', length: 255);
$table->string('token', length: 255)->nullable();
$table->timestamp('last_updated', precision: 0)->nullable();
$table->json('list_data')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('source');
}
};

View File

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('appeal', function (Blueprint $table) {
$table->bigIncrements('id');
$table->uuid('uuid');
$table->string('location', length: 255);
$table->string('location_admin', length: 255);
$table->string('sponsor', length: 255);
$table->text('description');
$table->boolean('reviewed')->default(false);
$table->boolean('approved')->default(false);
$table->timestamps(precision: 0);
$table->timestamp('deleted_at', precision: 0)->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('appeal');
}
};

View File

@ -3,6 +3,12 @@ section.index-search {
background: var(--white);
}
/* TODO: move to a global file? its not only for forms. */
section.index-search :focus-visible {
outline: 2px solid var(--white);
outline-offset: -4px;
}
section.start a.search-link {
color: var(--highlight);
font-size: 2.5em;
@ -12,36 +18,57 @@ section.start a.search-link {
}
form.index-search-form {
display: grid;
grid-template-columns: auto max-content;
gap: 15px;
width: 80%;
max-width: 1000px;
margin: 0 auto;
padding: 30px 0;
}
form.index-search-form > input,
form.index-search-form > button {
display: block;
margin: 0;
width: 100%;
height: 60px;
box-sizing: border-box;
}
form.index-search-form > input:focus {
color: var(--white);
}
form.index-search-form > input[type="text"] {
width: 88%;
height: 50px;
font: 44px var(--base-type);
}
form.index-search-form > input[type="hidden"] {
/* This removes it from the grid calculations */
position: absolute;
}
form.index-search-form > button {
height: 60px;
display: grid;
grid-template-columns: auto;
align-items: center;
padding: 0;
width: 60px;
position: relative;
top: 9px;
right: 0;
text-transform: uppercase;
}
form.index-search-form > button > img#search-icon {
float: none;
form.index-search-form > button > svg {
justify-self: center;
width: 48px;
height: 48px;
}
form.index-search-form > button > label {
font-weight: 500;
top: 15px;
position: relative;
font-size: 1.5em;
form.index-search-form > button > span {
display: none;
margin-top: 3px;
font-weight: 500;
font-size: 1.5em;
}
::placeholder {
@ -50,59 +77,65 @@ form.index-search-form > button > label {
}
section.index-meta article {
margin-top: 20px;
padding-block: 30px;
}
div.index-meta {
display: grid;
grid-template-columns: 50% 50%;
gap: 10px;
width: 98%;
font-weight: 500;
color: var(--secondary);
}
div.index-meta > label:nth-child(2),
div.index-meta > label:nth-child(4),
div.index-meta > label:nth-child(6) {
color: var(--white);
table.index-meta {
width: 100%;
border-collapse: collapse;
border-spacing: 0;
}
table.index-meta tr + tr > * {
padding-block-start: 10px;
}
table.index-meta :is(th, td) {
padding: 0;
font-weight: 500;
}
table.index-meta th {
color: var(--secondary);
text-align: left;
}
table.index-meta td {
padding-inline-start: 10px;
color: var(--white);
text-align: right;
}
@media only screen and (max-width: 800px) {
form.index-search-form > input[type="text"] {
width: 85%;
height: 50px;
font: 34px var(--base-type);
}
}
@media only screen and (max-width: 650px) {
form.index-search-form > input[type="text"] {
width: 80%;
height: 50px;
font: 34px var(--base-type);
}
}
@media only screen and (max-width: 480px) {
form.index-search-form {
grid-template-columns: auto;
}
form.index-search-form > input[type="text"] {
width: 99%;
height: 50px;
font: 27px var(--base-type);
}
form.index-search-form > input[type="text"],
form.index-search-form > button {
width: 99%;
top: 15px;
width: 100%;
}
form.index-search-form > button > label {
display: inline;
form.index-search-form > button {
grid-template-columns: auto 60px;
}
form.index-search-form > button > img#search-icon {
float: right;
form.index-search-form > button span {
display: block;
}
}

View File

@ -95,7 +95,7 @@ a.list-link > .item-block > .item-icon {
a.list-link {
display: grid;
grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
grid-template-rows: 100% 100px 30px 30px;
grid-template-rows: 1fr auto 30px 30px;
gap: 5px;
height: auto;
padding-bottom: 20px;

View File

@ -1,3 +1,4 @@
@import "../global/utilities.css";
@import "../global/colors.css";
@import "../global/forms.css";
@import "../global/typography.css";

View File

@ -8,16 +8,25 @@ input[type="text"] {
display: inline-block;
background: var(--white);
color: var(--primary);
transition: all 0.2s linear;
transition: 0.2s linear;
transition-property: color, background-color;
height: 30px;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: solid var(--highlight);
background-color: var(--highlight);
}
/* TODO: generalise this a bit */
button:focus,
input[type="submit"]:focus,
input[type="text"]:focus,
input[type="password"]:focus {
outline: 2px solid var(--white);
outline-offset: -4px;
}
textarea {
border: 0;
border-radius: 3px;
@ -34,8 +43,10 @@ input[type="submit"] {
position: relative;
cursor: pointer;
border: 0;
transition: all 0.3s linear;
transition: 0.3s linear;
transition-property: color, background-color;
height: 35px;
margin-top: 15px;
}
select {
@ -45,4 +56,5 @@ select {
appearance: none;
color: var(--primary);
background: var(--secondary);
height: 35px;
}

View File

@ -199,9 +199,7 @@ footer {
padding: 10px;
gap: 10px;
height: auto;
width: 80%;
margin: 20px auto;
max-width: 1000px;
width: auto;
position: relative;
}
@ -213,6 +211,15 @@ footer > div:nth-child(2) {
text-align: right;
}
/*
member stuff
*/
.your-avatar {
width: 250px;
border-radius: 5px;
}
/*
responsive
*/

View File

@ -67,6 +67,11 @@ h3 {
font-weight: 500;
}
h3.strong {
color: var(--secondary);
font-weight: bolder;
}
@media only screen and (max-width: 800px) {
h1 {
font-size: 2em;

View File

@ -0,0 +1,13 @@
/**
* Utilities
*/
.visually-hidden:not(:focus):not(:active) {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -1,8 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated by Pixelmator Pro 3.3.8 -->
<svg width="101" height="100" viewBox="0 0 101 100" xmlns="http://www.w3.org/2000/svg">
<g id="Group-copy">
<path id="Path-copy-2" fill="none" stroke="#efebe3" stroke-width="8.777778" stroke-linecap="round" stroke-linejoin="round" d="M 11 41.722221 C 11 58.68964 24.754808 72.444443 41.722221 72.444443 C 58.68964 72.444443 72.444443 58.68964 72.444443 41.722221 C 72.444443 24.754807 58.68964 11 41.722221 11 C 24.754808 11 11 24.754807 11 41.722221"/>
<path id="Path-copy" fill="none" stroke="#efebe3" stroke-width="8.777778" stroke-linecap="round" stroke-linejoin="round" d="M 90 90 L 63.666668 63.666668"/>
</g>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" id="search">
<path fill="none" stroke="currentcolor" stroke-width="8.777778" stroke-linecap="round" stroke-linejoin="round" d="M 11 41.722221 C 11 58.68964 24.754808 72.444443 41.722221 72.444443 C 58.68964 72.444443 72.444443 58.68964 72.444443 41.722221 C 72.444443 24.754807 58.68964 11 41.722221 11 C 24.754808 11 11 24.754807 11 41.722221"/>
<path fill="none" stroke="currentcolor" stroke-width="8.777778" stroke-linecap="round" stroke-linejoin="round" d="M 90 90 L 63.666668 63.666668"/>
</svg>

Before

Width:  |  Height:  |  Size: 729 B

After

Width:  |  Height:  |  Size: 571 B

View File

@ -11,8 +11,10 @@
@csrf
<label>Edit Location Name</label><br>
<input type="text" name="name" value="{{$location->name}}" /><br>
<label>Edit Location Comments</label><br>
<textarea name="description">{{$location->description}}</textarea><br>
<label>Edit Location Notes</label><br>
<textarea name="notes">{{$location->notes}}</textarea><br>
<label>Edit Reference Links (comma seperated)</label><br>
<textarea name="archive_links">{{$location->archive_links}}</textarea><br>
<label>Edit Reference Images</label><br>
@ -24,7 +26,7 @@
<h3>Images</h3>
@if($images != null)
@foreach($images as $image)
<a href="/{{$image->path}}" class="location-image" style="background: url(/{{$image->path}}) no-repeat center center / cover #fc6399" />
<a href="{{$image->path}}" class="location-image" style="background: url({{$image->path}}) no-repeat center center / cover #fc6399" />
</a>
@endforeach
@endif

View File

@ -2,12 +2,51 @@
@section('title', 'Den | Member Admin')
@section('main-content')
<section>
<article>
<h2>Member Listing </h2>
<a href="/den/admin/update">UPDATE LOCATIONS</a><br />
<a href="/den/admin/compile">COMPILE LOCATIONS</a>
</article>
</section>
@endsection
@php
switch($mode)
{
case 'member-create':
$action_url = '/den/member/create';
break;
case 'member-edit':
$action_url = '/den/member/edit';
break;
case 'admin-create':
$action_url = '/den/member/admin-create';
break;
}
@endphp
@section('main-content')
<section>
<article>
@switch($mode)
@case('member-edit')
<h2>Edit Info for {{$member->handle}}</h2>
@include('forms.member-edit')
<br />
@break
@case('member-create')
<h2>New Member Info</h2>
@include('forms.member-edit')
<br />
@break
@case('admin-create')
<h2>Make your first account</h2>
*This will be your administrator account.
@include('forms.member-edit')
<br />
@break
@default
<h2>Member Listing </h2>
@foreach($members as $member)
<a href="/den/member/{{$member->uuid}}">{{$member->handle}}</a><br />
@endforeach
<h2>Add Member </h2>
<a href="/den/member/edit/create">Make a new friend</a><br />
@endswitch
</article>
</section>
@endsection

View File

@ -0,0 +1,13 @@
@extends('frame')
@section('title', 'Den | Your Profile')
@section('main-content')
<section>
<article>
<h2>Edit Profile Deets </h2>
@include('forms.profile-edit')
</article>
</section>
<br />
@endsection

View File

@ -0,0 +1,42 @@
@extends('frame')
@section('title', 'Den | Sources Admin')
@php
switch($mode)
{
case 'source-create':
$action_url = '/den/source/create';
break;
case 'source-edit':
$action_url = '/den/source/edit';
break;
}
@endphp
@section('main-content')
<section>
<article>
@switch($mode)
@case('source-edit')
<h2>Edit Info for {{$source->url}}</h2>
@include('forms.source-edit')
<br />
@break
@case('source-create')
<h2>New Source Info</h2>
@include('forms.source-edit')
<br />
@break
@default
<h2>Current Sources </h2>
@foreach($sources as $source)
<a href="/den/source/{{$source->id}}">{{$source->url}}</a><br />
@endforeach
<h2>Add Source </h2>
<a href="/den/source/edit/create">Add a new Source</a><br />
@endswitch
</article>
</section>
@endsection

View File

@ -2,16 +2,17 @@
@section('title', 'Den | Start')
@section('main-content')
@section('main-content')
<section>
<article>
<h2>Hey {{$handle}} </h2>
<a href="/den/you">Edit Your Account</a><br />
<a href="/den/locations">Manage Locations</a><br />
@if($role==1)
<a href="/den/member">Manage Members</a><br />
@endif
</article>
</section>
@endsection
<section>
<article>
<h2>Hey {{$handle}} </h2>
<a href="/den/you">Edit Your Account</a><br />
@if($role==0)
<a href="/den/locations">Manage Locations</a><br />
<a href="/den/member">Manage Members</a><br />
<a href="/den/sources">Manage Sources</a><br />
@endif
</article>
</section>
@endsection

View File

@ -0,0 +1,74 @@
<form action="{{$action_url}}" method="post" enctype="multipart/form-data">
<div>
@php
isset($avatar) ? $avi = $avatar : $avi = '';
@endphp
<img class="your-avatar" src='{{$avi}}'>
<br />
<label>Handle</label><br />
@php
isset($member->handle) ? $handle = $member->handle : $handle = '';
@endphp
<input type="text" name="handle" value="{{$handle}}" />
<br />
@php
isset($member->email) ? $email = $member->email : $email = '';
@endphp
<label>Email</label><br />
<input type="text" name="email" value="{{$email}}" />
<br />
@php
isset($member->pronoun) ? $pronoun = $member->pronoun : $pronoun = '';
@endphp
<label>Pronouns</label><br />
<input type="text" name="pronouns" value="{{$pronoun}}" />
<br />
@php
isset($member->role) ? $role = $member->role : $role = 2;
//for creation of initial admin account
if($mode == 'admin-create')
{
$role = 0;
}
@endphp
@if($mode != 'admin-create')
<label>Role</label><br />
<input type="text" name="role" value="{{$role}}" />
<br />
@endif
@if($mode == 'member-create' || $mode == 'admin-create')
<label>Fresh Password</label><br />
<input type="password" id="fresh_pass" name="fresh_pass" value="" />
<br />
<label>Confirm Fresh Password</label><br />
<input type="password" id="fresh_pass_confirm" name="fresh_pass_confirm" value="" />
<br />
@endif
@php
isset($member->active) ? $status = $member->active : $status = false;
@endphp
@if($mode != 'admin-create')
<label>Status</label><br />
<select name="status">
@if($status)
<option value="true" selected>Active</option>
<option value="false">Not Active</option>
@else
<option value="true">Active</option>
<option value="false" selected>Not Active</option>
@endif
</select>
<br />
@endif
</div>
@csrf
@php
isset($member->uuid) ? $uuid = $member->uuid : $uuid = 0;
@endphp
<input type="hidden" name="id" value="{{$uuid}}" />
<input type="submit" value="Edit Member" name="submit_button">
</form>

View File

@ -0,0 +1,30 @@
<form action="/den/profile/edit" method="post" enctype="multipart/form-data">
<div>
<img class="your-avatar" src='{{$avatar}}'>
<br />
<label>New Avatar</label><br />
<input type="file" id="fresh_avi" name="fresh_avi" />
<br />
<label>Handle</label><br />
<input type="text" name="handle" value="{{$handle}}" />
<br />
<label>Email</label><br />
<input type="text" name="email" value="{{$email}}" />
<br />
<label>Pronouns</label><br />
<input type="text" name="pronouns" value="{{$pronouns}}" />
<br />
<h2>Change Password</h2>
<label>Fresh Password</label><br />
<input type="password" id="fresh_pass" name="fresh_pass" value="" />
<br />
<label>Confirm Fresh Password</label><br />
<input type="password" id="fresh_pass_confirm" name="fresh_pass_confirm" value="" />
<br />
</div>
@csrf
<input type="hidden" name="id" value="{{$uuid}}" />
<input type="submit" value="Edit Profile" name="submit_button">
</form>

View File

@ -0,0 +1,66 @@
<form action="{{$action_url}}" method="post" enctype="multipart/form-data">
<div>
@php
isset($source->url) ? $url = $source->url : $url = '';
@endphp
<label>URL</label><br />
<input type="text" name="url" value="{{$url}}" />
<br />
@php
isset($source->type) ? $type = $source->type : $type = '';
@endphp
<label>Type</label><br />
<select name="type">
@if($type == 'mastodon')
<option value="mastodon" selected>Mastodon</option>
<option value="gotosocial">GoToSocial</option>
@else
<option value="mastodon">Mastodon</option>
<option value="gotosocial" selected>GoToSocial</option>
@endif
</select>
<br />
@php
isset($source->format) ? $format = $source->format : $format = '';
@endphp
<label>Format</label><br />
<select name="format">
@if($format == 'json')
<option value="json" selected>JSON</option>
<option value="csv">CSV</option>
@else
<option value="json">JSON</option>
<option value="csv" selected>CSV</option>
@endif
</select>
<br />
@php
isset($source->active) ? $status = $source->active : $status = false;
@endphp
<label>Active?</label><br />
<select name="status">
@if($status)
<option value="true" selected>Active</option>
<option value="false">Not Active</option>
@else
<option value="true">Active</option>
<option value="false" selected>Not Active</option>
@endif
</select>
<br />
@php
isset($source->token) ? $token = $source->token : $token = '';
@endphp
<label>Access Token (enter 'none' to clear)</label><br />
<input type="text" name="token" value="{{$token}}" />
<br />
</div>
@csrf
@php
isset($source->id) ? $id = $source->id : $id = 0;
@endphp
<input type="hidden" name="id" value="{{$id}}" />
<input type="submit" value="Edit Source" name="submit_button">
</form>

View File

@ -2,100 +2,101 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="theme-color" content="#d66365" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
@yield('title')
</title>
@if(isset($front) && $front == false)
<link rel="stylesheet" type="text/css" href="/assets/css/back/start.css?=sdfsdf">
@elseif(!isset($front) || $front == true)
<link rel="stylesheet" type="text/css" href="/assets/css/front/start.css?=sdfsdf">
@endif
<meta charset="UTF-8">
<meta name="theme-color" content="#c3639e" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
@yield('title')
</title>
@if(isset($front) && $front == false)
<link rel="stylesheet" type="text/css" href="/assets/css/back/start.css?=sdfsdf">
@elseif(!isset($front) || $front == true)
<link rel="stylesheet" type="text/css" href="/assets/css/front/start.css?=sdfsdf">
@endif
</head>
<body>
<header>
<div>
<div class="header-left">
<a href="/">
<img src="/assets/images/global/logo-dark.svg" title="bad-space-logo" />
</a>
</div>
<div class="header-center">
<h1>{{$title}}</h1>
</div>
<div class="header-right">
<label for="element-toggle">
<img class="menu-icon" src="/assets/images/global/menu.svg" title="menu-open-toggle" />
</label>
<input id="element-toggle" type="checkbox" />
<div id="main-nav">
<nav>
<label for="element-toggle">
<img class="menu-icon" src="/assets/images/global/close.svg" title="menu-open-toggle" />
</label><br>
<a href="/" title="front" class="nav-links">
Front
</a><br />
<a href="/about" title="about" class="nav-links">
About
</a><br />
<a href="/listings/1" title="instance listing" class="nav-links">
Listings
</a><br />
<a href="/exports" title="list exports" class="nav-links">
Exports
</a><br />
<a href="/appeals" title="location appeals" class="nav-links">
Appeals
</a><br />
@if(Auth::check())
<a href="/den" title="den-start" class="nav-links">
Den
</a><br />
<a href="/logout" title="logout" class="nav-links">
Logout
</a><br />
@else
<a href="/den" title="login" class="nav-links">
The Den
</a><br />
@endif
</nav>
<script>
0
</script>
<header>
<div>
<div class="header-left">
<a href="/">
<img src="/assets/images/global/logo-dark.svg" alt="The Bad Space" />
</a>
</div>
<div class="header-center">
<h1>{{$title}}</h1>
</div>
<div class="header-right">
<label for="element-toggle">
<img class="menu-icon" src="/assets/images/global/menu.svg" title="menu-open-toggle" />
</label>
<input id="element-toggle" type="checkbox" />
<div id="main-nav">
<nav>
<label for="element-toggle">
<img class="menu-icon" src="/assets/images/global/close.svg" title="menu-open-toggle" />
</label><br>
<a href="/" title="front" class="nav-links">
Front
</a><br />
<a href="/about" title="about" class="nav-links">
About
</a><br />
<a href="/listings/1" title="instance listing" class="nav-links">
Listings
</a><br />
<a href="/exports" title="list exports" class="nav-links">
Exports
</a><br />
<a href="/appeals" title="location appeals" class="nav-links">
Appeals
</a><br />
@if(Auth::check())
<a href="/den" title="den-start" class="nav-links">
Den
</a><br />
<a href="/logout" title="logout" class="nav-links">
Logout
</a><br />
@else
<a href="/den" title="login" class="nav-links">
The Den
</a><br />
@endif
</nav>
</div>
</div>
</div>
</div>
</div>
</header>
</header>
@if($errors->any())
<div class="system-notice-error" role="status">
{{$errors->first()}}
</div>
@endif
@if(session('message'))
<div class="system-notice-message" role="status">
{!! session('message') !!}
</div>
@endif
@if($errors->any())
<div class="system-notice-error" role="status">
{{$errors->first()}}
</div>
@endif
@if(session('message'))
<div class="system-notice-message" role="status">
{!! session('message') !!}
</div>
@endif
<main>
@section('main-content')
@show
</main>
<footer>
<div>
The Bad Space © 2024<br />
an <a href="https://h-i.works">h.i.</a> project
</div>
<div>
a0.6
</div>
</footer>
</body>
<main>
@section('main-content')
@show
</main>
<footer>
<div>
The Bad Space © <?php echo date("Y"); ?><br />
an <a href="https://h-i.works">h.i.</a> project
</div>
<div>
a0.7
</div>
</footer>
</body>
</html>
</html>

View File

@ -1,89 +1,81 @@
@extends('frame')
@section('title', 'The Bad Space|About')
@section('main-content')
@parent
<section>
<article>
<h2 id="what">What is The Bad Space?</h2>
<p>The Bad Space arose from a need to identify instances that house bad actors, are poorly moderated, and/or contain inappropriate/offensive content (CSAM, hate speech, fascist ideology, etc.) that puts marginalized communities at risk.
</p>
<p>
It is an extension of the
<strong>#fediblock</strong>
hashtag - orginally created by
<a href="https://www.artistmarciax.com/">Artist Marcia X</a>
with additional support from
<a href="https://digital.rooting.garden">Ginger</a>
to provide a catalog of instances that seek to cause harm and reduce the quality of experience in the fediverse.
</p>
<p>
The searchable online catalog is built and maintained by
<a href="https://roiskinda.cool/profile.html">Ro</a>. The repo can be found <a href="https://koodu.h-i.works/projects/thebadspace">here</a>.
</p>
<p>Custom silence and suspend icons graciously provided by <a href="https://rage.love/@puf">puf</a>.</p>
<h2 id="how">How does it work?</h2>
<p>The Bad Space is a collaboration of communities, referred to as Current Sources, committed to actively moderating against racism, sexism, heterosexism, transphobia, ableism, casteism, or religion.</p>
@section('main-content')
@parent
<section>
<article>
<h2 id="what">What is The Bad Space?</h2>
<p>The Bad Space arose from a need to identify instances that house bad actors, are poorly moderated, and/or contain inappropriate/offensive content (CSAM, hate speech, fascist ideology, etc.) that puts marginalized communities at risk.</p>
<p>It is an extension of the <strong>#fediblock</strong> hashtag - orginally created by <a href="https://www.artistmarciax.com/">Artist Marcia X</a> with additional support from <a href="https://digital.rooting.garden">Ginger</a> to provide a catalog of instances that seek to cause harm and reduce the quality of experience in the fediverse.</p>
<p>The searchable online catalog is built and maintained by <a href="https://roiskinda.cool/profile.html">Ro</a>. The repo can be found <a href="https://koodu.h-i.works/projects/thebadspace">here</a>.</p>
<p>Custom silence and suspend icons graciously provided by <a href="https://rage.love/@puf">puf</a>.</p>
<p>These communities have permitted The Bad Space to ingest their respective blocklists detailing their silences and suspension to create a composite directory of sites that engage in the behavior(s) listed in the section above. For each behavior, the directory of locations can be searched and, through The Bad Space's public API, integrated into external services.</p>
<h2 id="how">How does it work?</h2>
<p>The Bad Space is a collaboration of communities, referred to as Current Sources, committed to actively moderating against racism, sexism, heterosexism, transphobia, ableism, casteism, or religion.</p>
<p>These communities have permitted The Bad Space to ingest their respective blocklists detailing their silences and suspension to create a composite directory of sites that engage in the behavior(s) listed in the section above. For each behavior, the directory of locations can be searched and, through The Bad Space's public API, integrated into external services.</p>
<h2>Adding Locations</h2>
<p>
Current Sources continually review the #fediblock hashtag and update their silences and suspensions when warranted. If an instance meets the criteria of a Current Source to be suspended or silenced, The Bad Space will automatically be updated according to said Current Sources' curated data.
<h2>Adding Locations</h2>
<p>Current Sources continually review the #fediblock hashtag and update their silences and suspensions when warranted. If an instance meets the criteria of a Current Source to be suspended or silenced, The Bad Space will automatically be updated according to said Current Sources' curated data.</p>
<p>For an instance to be listed on The Bad Space, at least two (2) Current Sources must have that location silenced and/or suspended. Instances will not display in the directory until two (2) Current Sources have taken moderation action against them.</p>
For an instance to be listed on The Bad Space, at least two (2) Current Sources must have that location silenced and/or suspended. Instances will not display in the directory until two (2) Current Sources have taken moderation action against them.
</p>
<h2>Removing Locations</h2>
Locations that are displayed in The Bad Space may petition to be removed from the catalog by sending an appeal request to The Bad Space. The appeal process is outlined <a href="/appeals">here</a>.
<h2>Removing Locations</h2>
<p>Locations that are displayed in The Bad Space may petition to be removed from the catalog by sending an appeal request to The Bad Space. The appeal process is outlined <a href="/appeals">here</a>.</p>
<p><strong>Current Sources:</strong></p>
Maston:<br />
@foreach($sources as $source)
@if($source->format == 'json')
<a href="https://{{$source->url}}">{{$source->url}}</a><br />
@endif
@endforeach
Custom CSV<br />
@foreach($sources as $source)
@if($source->format == 'csv')
<a href="{{$source->url}}">{{$source->url}}</a><br />
@endif
@endforeach
<h3 class="strong">Current Sources:</h3>
<h2>How do I use it?</h2>
<p>
The Bad Space is meant to be a resource for anyone looking to improve the quality of their online experience by creating a tool that catalogs sources for harassment and abuse. There are several options for how it can be used.
<h3>Search</h3>
To see if a site is listed in the database, use the
<a href="/">search feature</a>
to search for that URL. If it is in the database, information for that instance will be returned and associated instances if applicable.
<h3>CSV Exports</h3>
For a list of the current locations being tracked, click on one of the links below to download a dynamically generated CSV file that can be consumed as a blocklist. More formats will be added over time.
<br />
<a href="/exports/mastodon">For Mastodon</a>
<h3>API</h3>
The Bad Space has a public api that can be used to search the database programatically and return results in the JSON format. The API can be accsess at<br />
<code>https://thebad.space/api/v1/search</code>
by posting a JSON object with the following format:
<code>{"url":"search.url"}</code><br />
Data from API request will be returned in the follow format:<br />
<h4>Maston:</h4>
<ul>
@foreach($sources as $source)
@if($source->format == 'json')
<li><a href="https://{{$source->url}}">{{$source->url}}</a></li>
@else
<li>None</li>
@endif
@endforeach
</ul>
<pre>
<code>{
data:{
"listingCount":1,
"locations":
[
{
"url":"search.url",
"name":"Instance Name",
"description":"instance description",
"link":"bad-space-instance-link"
}
]
<h4>Custom CSV</h4>
<ul>
@foreach($sources as $source)
@if($source->format == 'csv')
<li><a href="{{$source->url}}">{{$source->url}}</a></li>
@else
<!--
<li>None</li>
-->
@endif
@endforeach
</ul>
<h2>How do I use it?</h2>
<p>The Bad Space is meant to be a resource for anyone looking to improve the quality of their online experience by creating a tool that catalogs sources for harassment and abuse. There are several options for how it can be used.</p>
<h3>Search</h3>
<p>To see if a site is listed in the database, use the <a href="/">search feature</a> to search for that URL. If it is in the database, information for that instance will be returned and associated instances if applicable.</p>
<h3>CSV Exports</h3>
<p>For a list of the current locations being tracked, click on one of the links below to download a dynamically generated CSV file that can be consumed as a blocklist. More formats will be added over time.</p>
<p><a href="/exports">Exports</a></p>
<h3>API</h3>
<p>The Bad Space has a public api that can be used to search the database programatically and return results in the JSON format. The API can be accsess at <code>https://thebad.space/api/v1/search</code> by posting a JSON object with the following format: <code>{"url":"search.url"}</code>. Data from API request will be returned in the follow format:</p>
<pre>
<code>{
data:{
"listingCount":1,
"locations":
[
{
"url":"search.url",
"name":"Instance Name",
"description":"instance description",
"link":"bad-space-instance-link"
}
}</code>
</pre>
</p>
</article>
</section>
@endsection
]
}
}</code>
</pre>
</article>
</section>
@endsection

View File

@ -1,62 +1,62 @@
@extends('frame')
@section('title', 'The Bad Space|Appeals')
@section('main-content')
@parent
<section>
<article>
<h2>Appeals</h2>
@section('main-content')
@parent
<section>
<article>
<h2>Appeals</h2>
Locations listed in The Bad Space have the right to appeal their inclusion if they feel they have been added unfairly.
Locations listed in The Bad Space have the right to appeal their inclusion if they feel they have been added unfairly.
<h3>Starting the Appeals Process</h3>
<h3>Starting the Appeals Process</h3>
<p>This process can be initiated by sending an Official Appeal to The Bad Space using the Appeal Form (found below) stating why they should not be included. All appeal requests must be sponsored by at least one Current Source, and their name(s) must be included in the submission.</p>
<p>This process can be initiated by sending an Official Appeal to The Bad Space using the Appeal Form (found below) stating why they should not be included. All appeal requests must be sponsored by at least one Current Source, and their name(s) must be included in the submission.</p>
<p>The Bad Space will validate the appeal request by contacting a moderator or administrator of the respective instance. If no moderator or administrator is detailed in the appeal request, it will be immediately disqualified.</p>
<p>The Bad Space will validate the appeal request by contacting a moderator or administrator of the respective instance. If no moderator or administrator is detailed in the appeal request, it will be immediately disqualified.</p>
<p>If the Appeal contains any threats, hate speech, microaggressions, taunts, or slurs, the Appeal will be automatically disqualified.</p>
<p>If the Appeal contains any threats, hate speech, microaggressions, taunts, or slurs, the Appeal will be automatically disqualified.</p>
An Appeal can only be made once every three (3) months.
An Appeal can only be made once every three (3) months.
<h3>Process Description</h3>
<h3>Process Description</h3>
<p>After an appeal request is validated, it will be reviewed by all Current Sources. Each Current Source will vote on whether to approve the requesting instance's appeal request.</p>
<p>After an appeal request is validated, it will be reviewed by all Current Sources. Each Current Source will vote on whether to approve the requesting instance's appeal request.</p>
<p>80% of Current Sources must vote in favor of the request to proceed to the next step. Appeals that do not meet this threshold will be rejected, and the administrator or moderator who petitioned for the appeal will be notified.
</p>
<p>80% of Current Sources must vote in favor of the request to proceed to the next step. Appeals that do not meet this threshold will be rejected, and the administrator or moderator who petitioned for the appeal will be notified.
</p>
<p>If an appeal is approved to proceed, a chat room will be created on the h.i. cloud community for the petitioner to be interviewed by Current Sources. Here, they will explain why they feel their instance should be removed from the database.</p>
<p>If an appeal is approved to proceed, a chat room will be created on the h.i. cloud community for the petitioner to be interviewed by Current Sources. Here, they will explain why they feel their instance should be removed from the database.</p>
The petitioner must include the following:
<ul>
<li>Verification of problematic content and members have been removed.</li>
<li>A public explanation of steps to recognize and remove problematic content and members.</li>
<li>A working plan to keep said location as free as possible from problematic members and content finding a home there.</li>
</ul>
The petitioner must include the following:
<ul>
<li>Verification of problematic content and members have been removed.</li>
<li>A public explanation of steps to recognize and remove problematic content and members.</li>
<li>A working plan to keep said location as free as possible from problematic members and content finding a home there.</li>
</ul>
<p>The petitioner also must be available to answer any questions from a member of Current Sources concerning the information provided.</p>
<p>The petitioner also must be available to answer any questions from a member of Current Sources concerning the information provided.</p>
<p>Failure to provide any of this information or refusing to be interviewed by Current Sources will result in an automatic disqualification.</p>
<p>Failure to provide any of this information or refusing to be interviewed by Current Sources will result in an automatic disqualification.</p>
<p>Upon completing the interview process, each Current Source will choose to remove or maintain their current block or silence action. The result of each choice will be reflected in the database itself, which can be publicly reviewed at any time.</p>
<p>Upon completing the interview process, each Current Source will choose to remove or maintain their current block or silence action. The result of each choice will be reflected in the database itself, which can be publicly reviewed at any time.</p>
<h2>Appeals Form</h2>
<h2>Appeals Form</h2>
<form action="/appeal" method="post" enctype="multipart/form-data">
@csrf
<label>Appeal Location</label><br />
<input type="text" name="location" value="" /><br />
<label>Appeal Location Admin</label><br />
<input type="text" name="location_admin" value="" /><br />
<label>Appeal Sponsor</label><br />
<input type="text" name="sponsor" value="" /><br />
<label>What is 1+1?</label><br />
<input type="text" name="question" value="" /><br />
<label>Appeal Summary</label><br />
<textarea name="appeal_description">Appeal Summary</textarea>
<input type="hidden" name="h1" value="" /><br />
<input type="submit" value="File Appeal" name="submit_button">
</form>
</article>
</section>
@endsection
<form action="/appeal" method="post" enctype="multipart/form-data">
@csrf
<label>Appeal Location</label><br />
<input type="text" name="location" value="" /><br />
<label>Appeal Location Admin</label><br />
<input type="text" name="location_admin" value="" /><br />
<label>Appeal Sponsor</label><br />
<input type="text" name="sponsor" value="" /><br />
<label>What is 1+1?</label><br />
<input type="text" name="question" value="" /><br />
<label>Appeal Summary</label><br />
<textarea name="appeal_description">Appeal Summary</textarea>
<input type="hidden" name="h1" value="" /><br />
<input type="submit" value="File Appeal" name="submit_button">
</form>
</article>
</section>
@endsection

View File

@ -1,23 +1,23 @@
@extends('frame')
@section('title', 'The Bad Space|Exports')
@section('main-content')
@parent
<section>
<article>
<h2>CSV Exports</h2>
Heat Rating is the percentage of Current Sources that have taken action against an instance. The higher the number of Sources that have silenced and/or suspended an instance, the higher the Heat Rating.*
<h3>For Mastodon</h3>
@section('main-content')
@parent
<section>
<article>
<h2>CSV Exports</h2>
Heat Rating is the percentage of Current Sources that have taken action against an instance. The higher the number of Sources that have silenced and/or suspended an instance, the higher the Heat Rating.*
<h3>For Mastodon</h3>
@foreach($list as $item)
@foreach($list as $item)
<a href="/exports/mastodon/{{$item['heatRating']}}">Heat Rating: {{$item['heatRating']}}% - Location Count: {{$item['ratingCount']}}</a><br />
<a href="/exports/mastodon/{{$item['heatRating']}}">Heat Rating: {{$item['heatRating']}}% - Location Count: {{$item['ratingCount']}}</a><br />
@endforeach
<br />
<i>* Heating Ratings are still a work in progress so please review list before using.</i>
<br /><br />
@endforeach
<br />
<i>* Heating Ratings are still a work in progress so please review list before using.</i>
<br /><br />
</article>
</section>
@endsection
</article>
</section>
@endsection

View File

@ -1,13 +1,15 @@
@extends('frame')
@extends('frame')
@section('title', 'This is The Bad Space')
@section('main-content')
@parent
<section class="index-search">
<form class="index-search-form" action="/search" method="post" enctype="multipart/form-data">
<input type="text" name="index_search" value="" placeholder="Hi! This is where you search." />
<button aria-label="search-button">
<label id="search-label">LOOK FOR IT</label>
<img id="search-icon" class="button-icon" src="assets/images/global/icon-search.svg" />
<button aria-labelledby="search-label">
<span id="search-label">Look for it</span>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg" role="img" aria-hidden="trye">
<use href="assets/images/global/icon-search.svg#search" />
</svg>
</button>
@csrf
</form>
@ -38,33 +40,21 @@
@endisset
<section class="index-meta">
<article>
<h2>Recent Updates</h2>
@foreach($recent as $item)
<a class="list-link" role="listitem" href="/location/{{$item->uuid}}">
@php
$rating = floor(($item->actions_count / $sources)*100);
@endphp
<span class="item-rating">{{$rating}}%</span>
<label class="item-name">{{$item->name}}</label>
<div class="item-silence">
<img class="item-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
{{$item->silence_count}}
</div>
<div class="item-block">
<img class="item-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
{{$item->block_count}}
</div>
</a>
@endforeach
<h2>Info</h2>
<div class="index-meta">
<label>Locations Tracked</label>
<label>{{$count}}</label>
<label>Total Sources</label>
<label>{{$sources}}</label>
<label>Latest Update</label>
<label>{{$latest_date}}</label>
</div>
<table class="index-meta">
<caption class="visually-hidden">Meta</caption>
<tr>
<th>Active Locations Tracked</th>
<td>{{$count}}</td>
</tr>
<tr>
<th>Total Sources</th>
<td>{{$sources}}</td>
</tr>
<tr>
<th>Latest Update</th>
<td>{{$latest_date}}</td>
</tr>
</table>
</article>
</section>
@endsection
@endsection

View File

@ -1,35 +1,35 @@
@extends('frame')
@section('title', 'The Bad Space|Listings')
@section('main-content')
@parent
<section>
<article role="list">
<h2>Page {{$pageNum}}</h2>
<a href="/listings/{{$prev}}">PREV</a>
{{$pageNum}} of {{$totalPages}}
<a href="/listings/{{$next}}">NEXT</a><br /><br />
@foreach($locations as $location)
@php
@section('main-content')
@parent
<section>
<article role="list">
<h2>Page {{$pageNum}}</h2>
<a href="/listings/{{$prev}}">PREV</a>
{{$pageNum}} of {{$totalPages}}
<a href="/listings/{{$next}}">NEXT</a>
@foreach($locations as $location)
@php
$action = $location->block_count + $location->silence_count;
$rating = floor(($action / $sources)*100);
@endphp
<a class="list-link" role="listitem" href="/location/{{$location->uuid}}">
<span class="item-rating">{{$rating}}%</span>
<label class="item-name">{{$location->name}}</label>
<div class="item-silence">
<img class="item-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
{{$location->silence_count}}
</div>
<div class="item-block">
<img class="item-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
{{$location->block_count}}
</div>
</a>
@endforeach
<br />
<a href="/listings/{{$prev}}">PREV</a>
{{$pageNum}} of {{$totalPages}}
<a href="/listings/{{$next}}">NEXT</a>
</article>
</section>
@endsection
@endphp
<a class="list-link" role="listitem" href="/location/{{$location->uuid}}">
<span class="item-rating">{{$rating}}%</span>
<label class="item-name">{{$location->name}}</label>
<div class="item-silence">
<img class="item-icon" src="/assets/images/global/status-silence.svg" title="silenced" />
{{$location->silence_count}}
</div>
<div class="item-block">
<img class="item-icon" src="/assets/images/global/status-suspend.svg" title="suspended" />
{{$location->block_count}}
</div>
</a>
@endforeach
<a href="/listings/{{$prev}}">PREV</a>
{{$pageNum}} of {{$totalPages}}
<a href="/listings/{{$next}}">NEXT</a>
<br /><br />
</article>
</section>
@endsection

View File

@ -6,13 +6,19 @@
@parent
<section>
<article>
<h2>Description</h2>
{{$location->description}}<br />
<h2>Public Comments</h2>
@foreach($comments as $comment)
@if($comment != " " && $comment != '')
{{trim($comment)}}<br /><br />
@endif
@endforeach
<h2>Notes</h2>
{{$location->notes}}
<h2>References</h2>
<h3>Images</h3>
@if($images != null)
@foreach($images as $image)
<a href="/{{$image->path}}" class="location-image" style="background: url(/{{$image->path}}) no-repeat center center / cover #fc6399" />
<a href="{{$image->path}}" class="location-image" style="background: url({{$image->path}}) no-repeat center center / cover #fc6399" />
</a>
@endforeach
@endif
@ -21,6 +27,9 @@
$rating = floor(($action / $sources_count)*100);
@endphp
<h3>Links</h3>
@foreach($links as $link)
<a href="{{$link}}">{{$link}}</a><br />
@endforeach
<div class="location-rating">
<div>
<img class="rating-icon" src="/assets/images/global/heat.svg" title="heat-rating" />
@ -47,8 +56,11 @@
<br />
Heat Rating is the percentage of <a href="/about#how">Current Sources</a> that have taken action against an instance. The higher the number of Sources that have silenced and/or suspended an instance, the higher the Heat Rating.
<br />
<br />UPDATED : {{$updated}}
<br />
<br />
</article>
</section>
@endsection

View File

@ -5,8 +5,10 @@ use App\Http\Controllers\FrontIndexController;
use App\Http\Controllers\AuthController;
use App\Http\Controllers\DenController;
use App\Http\Controllers\LocationController;
use App\Http\Controllers\MemberController;
use App\Http\Controllers\ExportController;
use App\Http\Controllers\AppealController;
use App\Http\Controllers\SourceController;
/*
|--------------------------------------------------------------------------
@ -27,6 +29,7 @@ Route::get("/location/{uuid}", [FrontIndexController::class, 'location']);
Route::get("/appeals", [FrontIndexController::class, 'appeals']);
Route::post("/search", [FrontIndexController::class, 'indexSearch']);
Route::post("/appeal", [AppealController::class, 'sendAppeal']);
Route::post("/den/member/admin-create", [MemberController::class, 'adminCreate']);
//exports
Route::get("/exports", [ExportController::class, 'exportIndex']);
@ -40,7 +43,6 @@ Route::get("/logout", [AuthController::class, 'leave']);
//back
Route::group(['prefix' => 'den', 'middleware' => 'member.check'], function () {
Route::get("/", [DenController::class, 'start']);
Route::get("/member", [DenController::class, 'member']);
Route::get("/listings/{pageNum}", [DenController::class, 'location']);
Route::get("/location/edit/{uuid}", [DenController::class, 'locationEdit']);
Route::get("/locations", [DenController::class, 'locations']);
@ -48,4 +50,19 @@ Route::group(['prefix' => 'den', 'middleware' => 'member.check'], function () {
Route::post("/locations/edit", [LocationController::class, 'editLocation']);
Route::get("/admin/update", [LocationController::class, 'updateLocations']);
Route::get("/admin/compile", [LocationController::class, 'compileLocations']);
//member stuff
Route::get("/you", [MemberController::class, 'profile']);
Route::get("/member", [MemberController::class, 'index']);
Route::get("/member/{uuid}", [MemberController::class, 'editMember']);
Route::get("/member/edit/create", [MemberController::class, 'createMember']);
//source stuff
Route::get("/sources", [SourceController::class, 'index']);
Route::get("/source/{id}", [SourceController::class, 'editSource']);
Route::get("/source/edit/create", [SourceController::class, 'createSource']);
//actions
Route::post("/profile/edit", [MemberController::class, 'profileEdit']);
Route::post("/member/edit", [MemberController::class, 'memberEdit']);
Route::post("/member/create", [MemberController::class, 'memberCreate']);
Route::post("/source/edit", [SourceController::class, 'sourceEdit']);
Route::post("/source/create", [SourceController::class, 'sourceCreate']);
});